forked from 1b-agent/opencadstudio-upstream
Dock: option B icon strip with an edge-band group grip
The icon strip follows the agreed "option B" concept: - Flat 40 px strip; groups are rows of their pallets' icons separated by hairline dividers, with no grip lines. The + button is a small round button under the last group. - The shown group is marked by a blue bar on its window-side edge and brighter icons. That 8 px edge band is the group grip: hovering it widens and lights the bar, tints the row, shows a grab cursor and a "Drag to move group" tooltip; dragging it moves the whole group. - Icons still drag single pallets: within their group or into another group (between its icons), or onto a divider to start a new group. Hovering an icon gives it a soft background and shows its name. - Drop feedback: a short accent line with end caps between icons means "join this group here" (the target group's bar lights up); a full-width line on a divider with a "+ New group" badge means "new group here". The dragged icon (or whole group) fades in place and a ghost follows the pointer. - The zone around a divider (a few px into the icons on either side) starts a new group; the lower half of a group's last icon still joins that group at the end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013RyGiMXcsf9HTKfqDxaQGt
This commit is contained in:
4 files changed
+438
-89
No files matched your search
@@ -876,6 +876,10 @@ pub(super) struct OpenCADStudio {
|
|||||||
/// Group being dragged by its strip grip (side, slot index). While set,
|
/// Group being dragged by its strip grip (side, slot index). While set,
|
||||||
/// `dock_dragging` holds the group's shown panel for the preview.
|
/// `dock_dragging` holds the group's shown panel for the preview.
|
||||||
pub(crate) dock_dragging_group: Option<(crate::app::config::DockSide, usize)>,
|
pub(crate) dock_dragging_group: Option<(crate::app::config::DockSide, usize)>,
|
||||||
|
/// Group whose edge band (group grip) the pointer is over.
|
||||||
|
pub(crate) dock_grip_hover: Option<(crate::app::config::DockSide, usize)>,
|
||||||
|
/// Pallet whose strip icon the pointer is over.
|
||||||
|
pub(crate) dock_icon_hover: Option<crate::ui::dock::PanelId>,
|
||||||
/// Reference-table column currently being width-resized (column index),
|
/// Reference-table column currently being width-resized (column index),
|
||||||
/// with the last pointer position. Mirrors the Layers Name-column drag.
|
/// with the last pointer position. Mirrors the Layers Name-column drag.
|
||||||
pub(crate) xref_col_drag: Option<usize>,
|
pub(crate) xref_col_drag: Option<usize>,
|
||||||
@@ -4318,6 +4322,8 @@ impl OpenCADStudio {
|
|||||||
dock_title_hover: None,
|
dock_title_hover: None,
|
||||||
dock_edge_menu: None,
|
dock_edge_menu: None,
|
||||||
dock_dragging_group: None,
|
dock_dragging_group: None,
|
||||||
|
dock_grip_hover: None,
|
||||||
|
dock_icon_hover: None,
|
||||||
xref_col_drag: None,
|
xref_col_drag: None,
|
||||||
xref_col_last: None,
|
xref_col_last: None,
|
||||||
xref_split_drag: false,
|
xref_split_drag: false,
|
||||||
|
|||||||
@@ -652,6 +652,14 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
|||||||
self.save_config();
|
self.save_config();
|
||||||
task
|
task
|
||||||
}
|
}
|
||||||
|
DockMsg::GripHover(grip) => {
|
||||||
|
self.dock_grip_hover = grip;
|
||||||
|
iced::Task::none()
|
||||||
|
}
|
||||||
|
DockMsg::IconHover(id) => {
|
||||||
|
self.dock_icon_hover = id;
|
||||||
|
iced::Task::none()
|
||||||
|
}
|
||||||
DockMsg::GroupGrab(side, gi) => {
|
DockMsg::GroupGrab(side, gi) => {
|
||||||
if let Some(&id) = self.dock_group_visible(side, gi).first() {
|
if let Some(&id) = self.dock_group_visible(side, gi).first() {
|
||||||
self.dock_begin_drag(id);
|
self.dock_begin_drag(id);
|
||||||
@@ -2232,10 +2240,15 @@ mod tests {
|
|||||||
app.dock.join_group(PanelId::BlockPalette, DockSide::Left, 0, 1);
|
app.dock.join_group(PanelId::BlockPalette, DockSide::Left, 0, 1);
|
||||||
let (layout, _) = app.dock_strip_layout(DockSide::Left);
|
let (layout, _) = app.dock_strip_layout(DockSide::Left);
|
||||||
let top = layout[0].icons_top;
|
let top = layout[0].icons_top;
|
||||||
// Drag the Properties icon below the Blocks icon.
|
// Drag the Properties icon onto the lower half of the Blocks icon:
|
||||||
|
// below it, still in the group (the divider under it would start a
|
||||||
|
// new group).
|
||||||
let _ = app.on_dock(DockMsg::IconPress(PanelId::Properties));
|
let _ = app.on_dock(DockMsg::IconPress(PanelId::Properties));
|
||||||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(18.0, top + 10.0)));
|
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(18.0, top + 10.0)));
|
||||||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(18.0, top + 2.0 * STRIP_CELL_H)));
|
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(
|
||||||
|
18.0,
|
||||||
|
top + 2.0 * STRIP_CELL_H - 10.0,
|
||||||
|
)));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.dock_drag_target,
|
app.dock_drag_target,
|
||||||
Some(DropTarget::Join {
|
Some(DropTarget::Join {
|
||||||
@@ -2251,6 +2264,41 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dock_dragging_an_icon_into_another_group_lands_between_its_icons() {
|
||||||
|
use crate::app::config::DockSide;
|
||||||
|
use crate::ui::dock::{DockGroup, DockMsg, DropTarget, PanelId, STRIP_CELL_H};
|
||||||
|
let mut app = dock_app();
|
||||||
|
app.show_browser = true;
|
||||||
|
app.dock.left = vec![
|
||||||
|
DockGroup::stack(vec![PanelId::Properties, PanelId::Browser]),
|
||||||
|
DockGroup::single(PanelId::BlockPalette),
|
||||||
|
];
|
||||||
|
app.dock.right.clear();
|
||||||
|
let (layout, _) = app.dock_strip_layout(DockSide::Left);
|
||||||
|
// Drag the Blocks icon (group 1) to between Properties and Browser.
|
||||||
|
let _ = app.on_dock(DockMsg::IconPress(PanelId::BlockPalette));
|
||||||
|
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(20.0, layout[1].icons_top + 10.0)));
|
||||||
|
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(
|
||||||
|
20.0,
|
||||||
|
layout[0].icons_top + STRIP_CELL_H + 2.0,
|
||||||
|
)));
|
||||||
|
assert_eq!(
|
||||||
|
app.dock_drag_target,
|
||||||
|
Some(DropTarget::Join {
|
||||||
|
side: DockSide::Left,
|
||||||
|
group: 0,
|
||||||
|
index: 1
|
||||||
|
})
|
||||||
|
);
|
||||||
|
let _ = app.on_dock(DockMsg::DragRelease);
|
||||||
|
assert_eq!(app.dock.left.len(), 1, "the emptied group goes away");
|
||||||
|
assert_eq!(
|
||||||
|
app.dock.left[0].panels,
|
||||||
|
vec![PanelId::Properties, PanelId::BlockPalette, PanelId::Browser]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dock_grip_drag_moves_a_whole_group() {
|
fn dock_grip_drag_moves_a_whole_group() {
|
||||||
use crate::app::config::DockSide;
|
use crate::app::config::DockSide;
|
||||||
@@ -2264,7 +2312,7 @@ mod tests {
|
|||||||
app.dock.right.clear();
|
app.dock.right.clear();
|
||||||
let (layout, plus_top) = app.dock_strip_layout(DockSide::Left);
|
let (layout, plus_top) = app.dock_strip_layout(DockSide::Left);
|
||||||
let _ = app.on_dock(DockMsg::GroupGrab(DockSide::Left, 0));
|
let _ = app.on_dock(DockMsg::GroupGrab(DockSide::Left, 0));
|
||||||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(18.0, layout[0].grip_top + 2.0)));
|
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(18.0, layout[0].top + 2.0)));
|
||||||
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(18.0, plus_top + 10.0)));
|
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(18.0, plus_top + 10.0)));
|
||||||
let _ = app.on_dock(DockMsg::DragRelease);
|
let _ = app.on_dock(DockMsg::DragRelease);
|
||||||
assert_eq!(app.dock.left[0].panels, vec![PanelId::BlockPalette]);
|
assert_eq!(app.dock.left[0].panels, vec![PanelId::BlockPalette]);
|
||||||
|
|||||||
+313
-50
@@ -2830,81 +2830,188 @@ impl OpenCADStudio {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An edge's vertical icon strip: per group a grip bar (drag it to move
|
/// An edge's vertical icon strip: per group its open pallets' icons in a
|
||||||
/// the group) over its open pallets' icons, the shown group framed on a
|
/// row ended by a hairline divider, then a + button opening the pallet
|
||||||
/// grey background, then a + button opening the pallet menu. Heights
|
/// menu. The band along a group's window-side edge is its grip: it
|
||||||
/// follow `dock::strip_layout`, which drop targeting uses too.
|
/// carries the blue bar of the shown group, lights up while hovered (or
|
||||||
|
/// while it is a drop target) and drags the whole group; an icon shows its
|
||||||
|
/// group on click and drags just that pallet. Heights follow
|
||||||
|
/// `dock::strip_layout`, which drop targeting uses too.
|
||||||
fn dock_icon_strip(&self, side: crate::app::config::DockSide) -> Element<'_, Message> {
|
fn dock_icon_strip(&self, side: crate::app::config::DockSide) -> Element<'_, Message> {
|
||||||
use crate::ui::dock::{DockMsg, DOCK_STRIP_W, STRIP_CELL_H, STRIP_GAP, STRIP_GRIP_H};
|
use crate::app::config::DockSide;
|
||||||
|
use crate::ui::dock::{
|
||||||
|
DockMsg, DropTarget, DOCK_STRIP_W, STRIP_CELL_H, STRIP_DIVIDER_H, STRIP_GRIP_W,
|
||||||
|
STRIP_PAD, STRIP_PLUS_GAP,
|
||||||
|
};
|
||||||
let shown = self.dock_shown_group(side);
|
let shown = self.dock_shown_group(side);
|
||||||
|
let dragging = self.dock_drag_target.is_some();
|
||||||
|
let target_group = match self.dock_drag_target {
|
||||||
|
Some(DropTarget::Join { side: s, group, .. }) if s == side => Some(group),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let tip_side = match side {
|
||||||
|
DockSide::Left => iced::widget::tooltip::Position::Right,
|
||||||
|
DockSide::Right => iced::widget::tooltip::Position::Left,
|
||||||
|
};
|
||||||
let mut items: Vec<Element<'_, Message>> = Vec::new();
|
let mut items: Vec<Element<'_, Message>> = Vec::new();
|
||||||
for gi in self.dock_visible_groups(side) {
|
for gi in self.dock_visible_groups(side) {
|
||||||
items.push(Space::new().height(STRIP_GAP).into());
|
let is_shown = shown == Some(gi);
|
||||||
let grip = container(
|
let lit = target_group == Some(gi)
|
||||||
container(Space::new())
|
|| (!dragging && self.dock_grip_hover == Some((side, gi)));
|
||||||
.width(Length::Fixed(18.0))
|
let group_dragged = dragging && self.dock_dragging_group == Some((side, gi));
|
||||||
.height(Length::Fixed(3.0))
|
let panels = self.dock_group_visible(side, gi);
|
||||||
.style(|theme: &Theme| container::Style {
|
let row_h = 2.0 * STRIP_PAD + panels.len() as f32 * STRIP_CELL_H;
|
||||||
background: Some(Background::Color(
|
|
||||||
theme.palette().background.strong.color,
|
let mut icons: Vec<Element<'_, Message>> = vec![Space::new().height(STRIP_PAD).into()];
|
||||||
)),
|
for id in panels {
|
||||||
|
let faded = group_dragged
|
||||||
|
|| (dragging
|
||||||
|
&& self.dock_dragging_group.is_none()
|
||||||
|
&& self.dock_dragging == Some(id));
|
||||||
|
let glyph = if faded {
|
||||||
|
crate::ui::icons::themed_disabled(id.icon(), 20.0)
|
||||||
|
} else if is_shown {
|
||||||
|
crate::ui::icons::themed(id.icon(), 20.0)
|
||||||
|
} else {
|
||||||
|
crate::ui::icons::themed_secondary(id.icon(), 20.0)
|
||||||
|
};
|
||||||
|
let hovered = !dragging && self.dock_icon_hover == Some(id);
|
||||||
|
let cell = container(glyph)
|
||||||
|
.center_x(Length::Fixed(STRIP_CELL_H))
|
||||||
|
.center_y(Length::Fixed(STRIP_CELL_H - 2.0))
|
||||||
|
.style(move |theme: &Theme| container::Style {
|
||||||
|
background: hovered
|
||||||
|
.then(|| Background::Color(theme.palette().background.weak.color)),
|
||||||
border: Border {
|
border: Border {
|
||||||
radius: 1.5.into(),
|
radius: 5.0.into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
});
|
||||||
)
|
|
||||||
.center_x(Length::Fixed(DOCK_STRIP_W))
|
|
||||||
.center_y(Length::Fixed(STRIP_GRIP_H));
|
|
||||||
let mut group_items: Vec<Element<'_, Message>> = vec![mouse_area(grip)
|
|
||||||
.on_press(Message::Dock(DockMsg::GroupGrab(side, gi)))
|
|
||||||
.interaction(iced::mouse::Interaction::Grab)
|
|
||||||
.into()];
|
|
||||||
for id in self.dock_group_visible(side, gi) {
|
|
||||||
let cell = mouse_area(
|
let cell = mouse_area(
|
||||||
container(crate::ui::icons::themed(id.icon(), 20.0))
|
container(cell)
|
||||||
.center_x(Length::Fixed(DOCK_STRIP_W))
|
.center_x(Length::Fixed(DOCK_STRIP_W))
|
||||||
.center_y(Length::Fixed(STRIP_CELL_H)),
|
.center_y(Length::Fixed(STRIP_CELL_H)),
|
||||||
)
|
)
|
||||||
.on_press(Message::Dock(DockMsg::IconPress(id)))
|
.on_press(Message::Dock(DockMsg::IconPress(id)))
|
||||||
.on_enter(Message::Dock(DockMsg::Hover(id)))
|
.on_enter(Message::Dock(DockMsg::IconHover(Some(id))))
|
||||||
|
.on_exit(Message::Dock(DockMsg::IconHover(None)))
|
||||||
.interaction(iced::mouse::Interaction::Pointer);
|
.interaction(iced::mouse::Interaction::Pointer);
|
||||||
group_items.push(
|
icons.push(
|
||||||
iced::widget::tooltip(
|
iced::widget::tooltip(cell, text(id.title()).size(10), tip_side)
|
||||||
cell,
|
.gap(4)
|
||||||
text(id.title()).size(10),
|
.into(),
|
||||||
match side {
|
|
||||||
crate::app::config::DockSide::Left => {
|
|
||||||
iced::widget::tooltip::Position::Right
|
|
||||||
}
|
|
||||||
crate::app::config::DockSide::Right => {
|
|
||||||
iced::widget::tooltip::Position::Left
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.gap(4)
|
|
||||||
.into(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let is_shown = shown == Some(gi);
|
|
||||||
|
// The group grip: a band on the window side holding the edge bar.
|
||||||
|
let bar_w = if lit { 5.0 } else { 3.0 };
|
||||||
|
let bar = container(Space::new().width(bar_w).height(Fill)).style(move |theme: &Theme| {
|
||||||
|
let palette = theme.palette();
|
||||||
|
let (color, glow) = if lit {
|
||||||
|
(Some(palette.primary.weak.color), true)
|
||||||
|
} else if is_shown {
|
||||||
|
(Some(palette.primary.base.color), false)
|
||||||
|
} else {
|
||||||
|
(None, false)
|
||||||
|
};
|
||||||
|
container::Style {
|
||||||
|
background: color.map(Background::Color),
|
||||||
|
border: Border {
|
||||||
|
radius: match side {
|
||||||
|
DockSide::Left => iced::border::Radius::default().right(3.0),
|
||||||
|
DockSide::Right => iced::border::Radius::default().left(3.0),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
shadow: if glow {
|
||||||
|
iced::Shadow {
|
||||||
|
color: palette.primary.base.color.scale_alpha(0.55),
|
||||||
|
offset: iced::Vector::new(0.0, 0.0),
|
||||||
|
blur_radius: 8.0,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
iced::Shadow::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let band = mouse_area(
|
||||||
|
container(bar)
|
||||||
|
.width(Length::Fixed(STRIP_GRIP_W))
|
||||||
|
.height(Fill)
|
||||||
|
.padding(iced::Padding {
|
||||||
|
top: 5.0,
|
||||||
|
bottom: 5.0,
|
||||||
|
left: 0.0,
|
||||||
|
right: 0.0,
|
||||||
|
})
|
||||||
|
.align_x(match side {
|
||||||
|
DockSide::Left => iced::alignment::Horizontal::Left,
|
||||||
|
DockSide::Right => iced::alignment::Horizontal::Right,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.on_press(Message::Dock(DockMsg::GroupGrab(side, gi)))
|
||||||
|
.on_enter(Message::Dock(DockMsg::GripHover(Some((side, gi)))))
|
||||||
|
.on_exit(Message::Dock(DockMsg::GripHover(None)))
|
||||||
|
.interaction(iced::mouse::Interaction::Grab);
|
||||||
|
let band = iced::widget::tooltip(
|
||||||
|
band,
|
||||||
|
text(t!("Drag to move group")).size(10),
|
||||||
|
tip_side,
|
||||||
|
)
|
||||||
|
.gap(4);
|
||||||
|
let band = container(band)
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill)
|
||||||
|
.align_x(match side {
|
||||||
|
DockSide::Left => iced::alignment::Horizontal::Left,
|
||||||
|
DockSide::Right => iced::alignment::Horizontal::Right,
|
||||||
|
});
|
||||||
items.push(
|
items.push(
|
||||||
container(column(group_items))
|
container(stack![column(icons), band])
|
||||||
.width(Length::Fixed(DOCK_STRIP_W))
|
.width(Length::Fixed(DOCK_STRIP_W))
|
||||||
|
.height(Length::Fixed(row_h))
|
||||||
.style(move |theme: &Theme| container::Style {
|
.style(move |theme: &Theme| container::Style {
|
||||||
// The shown group sits on the grey background.
|
background: lit.then(|| {
|
||||||
background: is_shown
|
Background::Color(theme.palette().primary.weak.color.scale_alpha(0.12))
|
||||||
.then(|| Background::Color(theme.palette().background.weak.color)),
|
}),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
items.push(
|
||||||
|
container(Space::new())
|
||||||
|
.width(Fill)
|
||||||
|
.height(Length::Fixed(STRIP_DIVIDER_H))
|
||||||
|
.style(|theme: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(
|
||||||
|
theme.palette().background.neutral.color,
|
||||||
|
)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
items.push(Space::new().height(STRIP_GAP).into());
|
items.push(Space::new().height(STRIP_PLUS_GAP).into());
|
||||||
let plus = button(crate::ui::icons::themed_secondary(crate::ui::icons::PLUS, 14.0))
|
let plus = button(crate::ui::icons::themed_secondary(crate::ui::icons::PLUS, 14.0))
|
||||||
.on_press(Message::Dock(DockMsg::EdgeMenu(Some(side))))
|
.on_press(Message::Dock(DockMsg::EdgeMenu(Some(side))))
|
||||||
.style(button::subtle)
|
.style(|theme: &Theme, status| button::Style {
|
||||||
.padding([5, 8]);
|
background: Some(Background::Color(match status {
|
||||||
|
button::Status::Hovered | button::Status::Pressed => {
|
||||||
|
theme.palette().background.weak.color
|
||||||
|
}
|
||||||
|
_ => theme.palette().background.weakest.color,
|
||||||
|
})),
|
||||||
|
border: Border {
|
||||||
|
radius: 14.0.into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.width(Length::Fixed(28.0))
|
||||||
|
.height(Length::Fixed(28.0))
|
||||||
|
.padding(7);
|
||||||
items.push(container(plus).center_x(Length::Fixed(DOCK_STRIP_W)).into());
|
items.push(container(plus).center_x(Length::Fixed(DOCK_STRIP_W)).into());
|
||||||
container(column(items))
|
container(column(items))
|
||||||
.width(Length::Fixed(DOCK_STRIP_W))
|
.width(Length::Fixed(DOCK_STRIP_W))
|
||||||
@@ -3145,7 +3252,12 @@ impl OpenCADStudio {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Some((side, after)) = docked {
|
// A whole group shows only the strip feedback and its ghost; the
|
||||||
|
// column keeps showing what is there.
|
||||||
|
let group_drag = self.dock_dragging_group;
|
||||||
|
let docked = docked.map(|(side, after)| (side, group_drag.is_none().then_some(after)));
|
||||||
|
let marks_side = docked.as_ref().map(|(side, _)| *side);
|
||||||
|
if let Some((side, Some(after))) = docked {
|
||||||
// Lay the group out as it will be after the drop.
|
// Lay the group out as it will be after the drop.
|
||||||
if let Some((_, gi)) = after.location(id) {
|
if let Some((_, gi)) = after.location(id) {
|
||||||
let group = &after.groups(side)[gi];
|
let group = &after.groups(side)[gi];
|
||||||
@@ -3172,11 +3284,162 @@ impl OpenCADStudio {
|
|||||||
layers.push(place_at(dock_ghost(&[id], id, w, bottom - top), x, top));
|
layers.push(place_at(dock_ghost(&[id], id, w, bottom - top), x, top));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// On top: the strip's drop marks and a ghost of what is dragged
|
||||||
|
// following the pointer.
|
||||||
|
if let Some(side) = marks_side {
|
||||||
|
layers.extend(self.dock_strip_drop_marks(side, target));
|
||||||
|
if let Some(p) = self.dock_drag_last {
|
||||||
|
let icons = match group_drag {
|
||||||
|
Some((s, g)) => self.dock_group_visible(s, g),
|
||||||
|
None => vec![id],
|
||||||
|
};
|
||||||
|
layers.push(place_at(dock_icon_ghost(&icons), p.x - 16.0, p.y - 15.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
iced::widget::Stack::with_children(layers)
|
iced::widget::Stack::with_children(layers)
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
.height(Fill)
|
.height(Fill)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The icon strip's drop marks for `target` on `side`: a short line with
|
||||||
|
/// end caps between a group's icons (join that group there), or a
|
||||||
|
/// full-width line on a divider with a "+ New group" badge.
|
||||||
|
fn dock_strip_drop_marks(
|
||||||
|
&self,
|
||||||
|
side: crate::app::config::DockSide,
|
||||||
|
target: crate::ui::dock::DropTarget,
|
||||||
|
) -> Vec<Element<'_, Message>> {
|
||||||
|
use crate::app::config::DockSide;
|
||||||
|
use crate::ui::dock::{DropTarget, DOCK_STRIP_W, STRIP_CELL_H};
|
||||||
|
let (ww, _) = self.dock_workspace_size();
|
||||||
|
let strip_x = match side {
|
||||||
|
DockSide::Left => 0.0,
|
||||||
|
DockSide::Right => ww - DOCK_STRIP_W,
|
||||||
|
};
|
||||||
|
let (layout, _) = self.dock_strip_layout(side);
|
||||||
|
let mut marks: Vec<Element<'_, Message>> = Vec::new();
|
||||||
|
match target {
|
||||||
|
DropTarget::Join { group, index, .. } => {
|
||||||
|
let Some(g) = layout.iter().find(|g| g.group == group) else {
|
||||||
|
return marks;
|
||||||
|
};
|
||||||
|
// Icons above the insertion point, counted as the strip shows
|
||||||
|
// them (the dragged icon still sits in its old place).
|
||||||
|
let dragged = self.dock_dragging;
|
||||||
|
let above = self.dock.groups(side)[group].panels[..index]
|
||||||
|
.iter()
|
||||||
|
.filter(|p| self.dock_panel_visible(**p) || Some(**p) == dragged)
|
||||||
|
.count();
|
||||||
|
let y = g.icons_top + above as f32 * STRIP_CELL_H;
|
||||||
|
marks.push(place_at(dock_insert_line(), strip_x + 7.0, y - 3.5));
|
||||||
|
}
|
||||||
|
DropTarget::Edge { index, .. } => {
|
||||||
|
let y = layout
|
||||||
|
.iter()
|
||||||
|
.find(|g| g.group >= index)
|
||||||
|
.map(|g| g.top)
|
||||||
|
.or_else(|| layout.last().map(|g| g.bottom()))
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
marks.push(place_at(
|
||||||
|
container(Space::new())
|
||||||
|
.width(Length::Fixed(DOCK_STRIP_W))
|
||||||
|
.height(Length::Fixed(4.0))
|
||||||
|
.style(dock_drop_line_style)
|
||||||
|
.into(),
|
||||||
|
strip_x,
|
||||||
|
(y - 2.0).max(0.0),
|
||||||
|
));
|
||||||
|
const BADGE_W: f32 = 96.0;
|
||||||
|
let badge = container(text(format!("+ {}", t!("New group"))).size(10))
|
||||||
|
.width(Length::Fixed(BADGE_W))
|
||||||
|
.center_x(Length::Fixed(BADGE_W))
|
||||||
|
.padding([2, 6])
|
||||||
|
.style(|theme: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(theme.palette().primary.base.color)),
|
||||||
|
text_color: Some(theme.palette().primary.base.text),
|
||||||
|
border: Border {
|
||||||
|
radius: 9.0.into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
let badge_x = match side {
|
||||||
|
DockSide::Left => DOCK_STRIP_W + 6.0,
|
||||||
|
DockSide::Right => ww - DOCK_STRIP_W - 6.0 - BADGE_W,
|
||||||
|
};
|
||||||
|
marks.push(place_at(badge.into(), badge_x, (y - 9.0).max(0.0)));
|
||||||
|
}
|
||||||
|
DropTarget::Float { .. } => {}
|
||||||
|
}
|
||||||
|
marks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The glowing accent used by the strip's drop lines.
|
||||||
|
fn dock_drop_line_style(theme: &Theme) -> container::Style {
|
||||||
|
container::Style {
|
||||||
|
background: Some(Background::Color(theme.palette().primary.base.color)),
|
||||||
|
border: Border {
|
||||||
|
radius: 2.0.into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
shadow: iced::Shadow {
|
||||||
|
color: theme.palette().primary.base.color.scale_alpha(0.8),
|
||||||
|
offset: iced::Vector::new(0.0, 0.0),
|
||||||
|
blur_radius: 6.0,
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The short insertion line with end caps shown between a group's icons.
|
||||||
|
fn dock_insert_line() -> Element<'static, Message> {
|
||||||
|
let cap = || {
|
||||||
|
container(Space::new())
|
||||||
|
.width(Length::Fixed(3.0))
|
||||||
|
.height(Length::Fixed(7.0))
|
||||||
|
.style(dock_drop_line_style)
|
||||||
|
};
|
||||||
|
row![
|
||||||
|
cap(),
|
||||||
|
container(Space::new())
|
||||||
|
.width(Length::Fixed(20.0))
|
||||||
|
.height(Length::Fixed(3.0))
|
||||||
|
.style(dock_drop_line_style),
|
||||||
|
cap(),
|
||||||
|
]
|
||||||
|
.align_y(iced::Center)
|
||||||
|
.height(Length::Fixed(7.0))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The ghost following the pointer while pallets are dragged: their icons
|
||||||
|
/// in an accent-framed box.
|
||||||
|
fn dock_icon_ghost(icons: &[crate::ui::dock::PanelId]) -> Element<'static, Message> {
|
||||||
|
let glyphs: Vec<Element<'static, Message>> = icons
|
||||||
|
.iter()
|
||||||
|
.map(|id| {
|
||||||
|
container(crate::ui::icons::themed(id.icon(), 18.0))
|
||||||
|
.center_x(Length::Fixed(32.0))
|
||||||
|
.center_y(Length::Fixed(30.0))
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
container(column(glyphs))
|
||||||
|
.style(|theme: &Theme| {
|
||||||
|
let palette = theme.palette();
|
||||||
|
container::Style {
|
||||||
|
background: Some(Background::Color(palette.primary.base.color.scale_alpha(0.22))),
|
||||||
|
border: Border {
|
||||||
|
color: palette.primary.base.color,
|
||||||
|
width: 1.5,
|
||||||
|
radius: 6.0.into(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Document tab bar ───────────────────────────────────────────────────────
|
// ── Document tab bar ───────────────────────────────────────────────────────
|
||||||
|
|||||||
+68
-36
@@ -51,8 +51,12 @@ pub enum DockMsg {
|
|||||||
/// Pallet menu pick: hide `panel` when it shows, else open it docked on
|
/// Pallet menu pick: hide `panel` when it shows, else open it docked on
|
||||||
/// the edge (as a new group unless it already has a group there).
|
/// the edge (as a new group unless it already has a group there).
|
||||||
EdgeMenuToggle(DockSide, PanelId),
|
EdgeMenuToggle(DockSide, PanelId),
|
||||||
/// Begin dragging a whole group (slot) by its grip in the icon strip.
|
/// Begin dragging a whole group by its edge band in the icon strip.
|
||||||
GroupGrab(DockSide, usize),
|
GroupGrab(DockSide, usize),
|
||||||
|
/// The pointer entered (`Some`) or left (`None`) a group's edge band.
|
||||||
|
GripHover(Option<(DockSide, usize)>),
|
||||||
|
/// The pointer entered (`Some`) or left (`None`) a pallet's strip icon.
|
||||||
|
IconHover(Option<PanelId>),
|
||||||
/// Double-click on a docked title bar: float the panel.
|
/// Double-click on a docked title bar: float the panel.
|
||||||
FloatOut(PanelId),
|
FloatOut(PanelId),
|
||||||
/// Double-click on a floating title bar: dock the panel on that side.
|
/// Double-click on a floating title bar: dock the panel on that side.
|
||||||
@@ -727,67 +731,85 @@ pub const DOCK_EDGE_ZONE: f32 = 48.0;
|
|||||||
/// Width of the grabbable divider between a docked slot and the viewport.
|
/// Width of the grabbable divider between a docked slot and the viewport.
|
||||||
pub const DOCK_DIVIDER_W: f32 = 5.0;
|
pub const DOCK_DIVIDER_W: f32 = 5.0;
|
||||||
/// Width of an edge's vertical icon strip (the groups' tabs).
|
/// Width of an edge's vertical icon strip (the groups' tabs).
|
||||||
pub const DOCK_STRIP_W: f32 = 36.0;
|
pub const DOCK_STRIP_W: f32 = 40.0;
|
||||||
/// Height of one pallet icon in the strip.
|
/// Height of one pallet icon in the strip.
|
||||||
pub const STRIP_CELL_H: f32 = 32.0;
|
pub const STRIP_CELL_H: f32 = 32.0;
|
||||||
/// Height of the grip bar above each group's icons.
|
/// Space above and below a group's icons, inside the group.
|
||||||
pub const STRIP_GRIP_H: f32 = 10.0;
|
pub const STRIP_PAD: f32 = 6.0;
|
||||||
/// Space above the first group and between groups in the strip.
|
/// Hairline divider between groups.
|
||||||
pub const STRIP_GAP: f32 = 6.0;
|
pub const STRIP_DIVIDER_H: f32 = 1.0;
|
||||||
|
/// Space between the last group and the + button.
|
||||||
|
pub const STRIP_PLUS_GAP: f32 = 8.0;
|
||||||
|
/// Width of the band along a group's window-side edge that drags the whole
|
||||||
|
/// group (the icons drag single pallets).
|
||||||
|
pub const STRIP_GRIP_W: f32 = 8.0;
|
||||||
|
/// How far the "new group" zone around a divider reaches into the icons on
|
||||||
|
/// either side.
|
||||||
|
const STRIP_NEW_GROUP_SLOP: f32 = 4.0;
|
||||||
|
|
||||||
/// Where one group sits in an edge's icon strip, top to bottom.
|
/// Where one group sits in an edge's icon strip, top to bottom.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct StripGroup {
|
pub struct StripGroup {
|
||||||
/// Slot index on the edge.
|
/// Group index on the edge.
|
||||||
pub group: usize,
|
pub group: usize,
|
||||||
/// Top of the group's grip bar.
|
/// Top of the group's row (just below the divider above it).
|
||||||
pub grip_top: f32,
|
pub top: f32,
|
||||||
/// Top of the group's first icon.
|
/// Top of the group's first icon.
|
||||||
pub icons_top: f32,
|
pub icons_top: f32,
|
||||||
/// Number of icons (visible tabs).
|
/// Number of icons (open pallets).
|
||||||
pub icons: usize,
|
pub icons: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lay out the icon strip for `groups` = (slot index, visible tab count),
|
impl StripGroup {
|
||||||
|
/// Bottom of the group's last icon.
|
||||||
|
pub fn icons_bottom(&self) -> f32 {
|
||||||
|
self.icons_top + self.icons as f32 * STRIP_CELL_H
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bottom of the group's row (where its divider starts).
|
||||||
|
pub fn bottom(&self) -> f32 {
|
||||||
|
self.icons_bottom() + STRIP_PAD
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lay out the icon strip for `groups` = (group index, open pallet count),
|
||||||
/// returning each group's place and the top of the + button below them.
|
/// returning each group's place and the top of the + button below them.
|
||||||
pub fn strip_layout(groups: &[(usize, usize)]) -> (Vec<StripGroup>, f32) {
|
pub fn strip_layout(groups: &[(usize, usize)]) -> (Vec<StripGroup>, f32) {
|
||||||
let mut y = STRIP_GAP;
|
let mut y = 0.0;
|
||||||
let placed = groups
|
let placed = groups
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&(group, icons)| {
|
.map(|&(group, icons)| {
|
||||||
let grip_top = y;
|
let g = StripGroup {
|
||||||
let icons_top = grip_top + STRIP_GRIP_H;
|
|
||||||
y = icons_top + icons as f32 * STRIP_CELL_H + STRIP_GAP;
|
|
||||||
StripGroup {
|
|
||||||
group,
|
group,
|
||||||
grip_top,
|
top: y,
|
||||||
icons_top,
|
icons_top: y + STRIP_PAD,
|
||||||
icons,
|
icons,
|
||||||
}
|
};
|
||||||
|
y = g.bottom() + STRIP_DIVIDER_H;
|
||||||
|
g
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
(placed, y)
|
(placed, y + STRIP_PLUS_GAP)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a dragged pallet released at height `y` over the icon strip joins.
|
/// What a dragged pallet released at height `y` over the icon strip joins.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum StripHit {
|
pub enum StripHit {
|
||||||
/// A new group before slot `group`; `None` = after the last group.
|
/// A new group before group `group`; `None` = after the last group.
|
||||||
NewGroup(Option<usize>),
|
NewGroup(Option<usize>),
|
||||||
/// Group `group`, at position `index` among its visible icons.
|
/// Group `group`, at position `index` among its open pallets.
|
||||||
Tab { group: usize, index: usize },
|
Tab { group: usize, index: usize },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Classify a pointer at strip height `y`: over a group's grip (or the gap
|
/// Classify a pointer at strip height `y`: on a divider (or the gap above
|
||||||
/// above it) starts a new group there, over its icons joins it between the
|
/// the first group) starts a new group there, over a group's icons joins it
|
||||||
/// nearest icons, below every group starts a new last group.
|
/// between the nearest icons, below every group starts a new last group.
|
||||||
pub fn strip_hit(y: f32, layout: &[StripGroup]) -> StripHit {
|
pub fn strip_hit(y: f32, layout: &[StripGroup]) -> StripHit {
|
||||||
for g in layout {
|
for g in layout {
|
||||||
if y < g.icons_top {
|
if y < g.icons_top + STRIP_NEW_GROUP_SLOP {
|
||||||
return StripHit::NewGroup(Some(g.group));
|
return StripHit::NewGroup(Some(g.group));
|
||||||
}
|
}
|
||||||
let bottom = g.icons_top + g.icons as f32 * STRIP_CELL_H + STRIP_GAP * 0.5;
|
if y < g.icons_bottom() - STRIP_NEW_GROUP_SLOP {
|
||||||
if y < bottom {
|
|
||||||
let index = (((y - g.icons_top) / STRIP_CELL_H).round().max(0.0) as usize)
|
let index = (((y - g.icons_top) / STRIP_CELL_H).round().max(0.0) as usize)
|
||||||
.min(g.icons);
|
.min(g.icons);
|
||||||
return StripHit::Tab {
|
return StripHit::Tab {
|
||||||
@@ -1211,23 +1233,33 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn strip_hit_maps_grips_icons_and_the_end() {
|
fn strip_hit_maps_dividers_icons_and_the_end() {
|
||||||
// Two groups: three icons, then one.
|
// Two groups: three icons, then one.
|
||||||
let (layout, plus_top) = strip_layout(&[(0, 3), (2, 1)]);
|
let (layout, plus_top) = strip_layout(&[(0, 3), (2, 1)]);
|
||||||
let g0 = layout[0];
|
let g0 = layout[0];
|
||||||
let g1 = layout[1];
|
let g1 = layout[1];
|
||||||
assert_eq!(g0.grip_top, STRIP_GAP);
|
assert_eq!(g0.icons_top, STRIP_PAD);
|
||||||
assert_eq!(g1.icons_top, g1.grip_top + STRIP_GRIP_H);
|
assert_eq!(g1.top, g0.bottom() + STRIP_DIVIDER_H);
|
||||||
assert_eq!(plus_top, g1.icons_top + STRIP_CELL_H + STRIP_GAP);
|
assert_eq!(plus_top, g1.bottom() + STRIP_DIVIDER_H + STRIP_PLUS_GAP);
|
||||||
// On the first grip: a new group before slot 0.
|
// Above the first icon: a new group before group 0.
|
||||||
assert_eq!(strip_hit(g0.grip_top + 2.0, &layout), StripHit::NewGroup(Some(0)));
|
assert_eq!(strip_hit(2.0, &layout), StripHit::NewGroup(Some(0)));
|
||||||
|
// Top half of the first icon: into group 0 at the top.
|
||||||
|
assert_eq!(
|
||||||
|
strip_hit(g0.icons_top + 10.0, &layout),
|
||||||
|
StripHit::Tab { group: 0, index: 0 }
|
||||||
|
);
|
||||||
// Between the first and second icon of group 0.
|
// Between the first and second icon of group 0.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
strip_hit(g0.icons_top + STRIP_CELL_H + 4.0, &layout),
|
strip_hit(g0.icons_top + STRIP_CELL_H + 4.0, &layout),
|
||||||
StripHit::Tab { group: 0, index: 1 }
|
StripHit::Tab { group: 0, index: 1 }
|
||||||
);
|
);
|
||||||
// On the second group's grip: a new group before slot 2.
|
// Lower half of the last icon: into group 0 at the end.
|
||||||
assert_eq!(strip_hit(g1.grip_top + 1.0, &layout), StripHit::NewGroup(Some(2)));
|
assert_eq!(
|
||||||
|
strip_hit(g0.icons_bottom() - 8.0, &layout),
|
||||||
|
StripHit::Tab { group: 0, index: 3 }
|
||||||
|
);
|
||||||
|
// On the divider between the groups: a new group before group 2.
|
||||||
|
assert_eq!(strip_hit(g0.bottom(), &layout), StripHit::NewGroup(Some(2)));
|
||||||
// Below everything: a new last group.
|
// Below everything: a new last group.
|
||||||
assert_eq!(strip_hit(plus_top + 20.0, &layout), StripHit::NewGroup(None));
|
assert_eq!(strip_hit(plus_top + 20.0, &layout), StripHit::NewGroup(None));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user