diff --git a/src/app/mod.rs b/src/app/mod.rs index a8279c317..9f989d392 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -876,6 +876,10 @@ pub(super) struct OpenCADStudio { /// Group being dragged by its strip grip (side, slot index). While set, /// `dock_dragging` holds the group's shown panel for the preview. 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, /// Reference-table column currently being width-resized (column index), /// with the last pointer position. Mirrors the Layers Name-column drag. pub(crate) xref_col_drag: Option, @@ -4318,6 +4322,8 @@ impl OpenCADStudio { dock_title_hover: None, dock_edge_menu: None, dock_dragging_group: None, + dock_grip_hover: None, + dock_icon_hover: None, xref_col_drag: None, xref_col_last: None, xref_split_drag: false, diff --git a/src/app/update/dialog.rs b/src/app/update/dialog.rs index 752f8ab2c..d394215e4 100644 --- a/src/app/update/dialog.rs +++ b/src/app/update/dialog.rs @@ -652,6 +652,14 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven self.save_config(); 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) => { if let Some(&id) = self.dock_group_visible(side, gi).first() { self.dock_begin_drag(id); @@ -2232,10 +2240,15 @@ mod tests { app.dock.join_group(PanelId::BlockPalette, DockSide::Left, 0, 1); let (layout, _) = app.dock_strip_layout(DockSide::Left); 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::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!( app.dock_drag_target, 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] fn dock_grip_drag_moves_a_whole_group() { use crate::app::config::DockSide; @@ -2264,7 +2312,7 @@ mod tests { app.dock.right.clear(); let (layout, plus_top) = app.dock_strip_layout(DockSide::Left); 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::DragRelease); assert_eq!(app.dock.left[0].panels, vec![PanelId::BlockPalette]); diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index c52b94f3f..eb069b62d 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -2830,81 +2830,188 @@ impl OpenCADStudio { ) } - /// An edge's vertical icon strip: per group a grip bar (drag it to move - /// the group) over its open pallets' icons, the shown group framed on a - /// grey background, then a + button opening the pallet menu. Heights - /// follow `dock::strip_layout`, which drop targeting uses too. + /// An edge's vertical icon strip: per group its open pallets' icons in a + /// row ended by a hairline divider, then a + button opening the pallet + /// menu. The band along a group's window-side edge is its grip: it + /// 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> { - 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 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> = Vec::new(); for gi in self.dock_visible_groups(side) { - items.push(Space::new().height(STRIP_GAP).into()); - let grip = container( - container(Space::new()) - .width(Length::Fixed(18.0)) - .height(Length::Fixed(3.0)) - .style(|theme: &Theme| container::Style { - background: Some(Background::Color( - theme.palette().background.strong.color, - )), + let is_shown = shown == Some(gi); + let lit = target_group == Some(gi) + || (!dragging && self.dock_grip_hover == Some((side, gi))); + let group_dragged = dragging && self.dock_dragging_group == Some((side, gi)); + let panels = self.dock_group_visible(side, gi); + let row_h = 2.0 * STRIP_PAD + panels.len() as f32 * STRIP_CELL_H; + + let mut icons: Vec> = 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 { - radius: 1.5.into(), + radius: 5.0.into(), ..Default::default() }, ..Default::default() - }), - ) - .center_x(Length::Fixed(DOCK_STRIP_W)) - .center_y(Length::Fixed(STRIP_GRIP_H)); - let mut group_items: Vec> = 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( - container(crate::ui::icons::themed(id.icon(), 20.0)) + container(cell) .center_x(Length::Fixed(DOCK_STRIP_W)) .center_y(Length::Fixed(STRIP_CELL_H)), ) .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); - group_items.push( - iced::widget::tooltip( - cell, - text(id.title()).size(10), - 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(), + icons.push( + iced::widget::tooltip(cell, text(id.title()).size(10), tip_side) + .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( - container(column(group_items)) + container(stack![column(icons), band]) .width(Length::Fixed(DOCK_STRIP_W)) + .height(Length::Fixed(row_h)) .style(move |theme: &Theme| container::Style { - // The shown group sits on the grey background. - background: is_shown - .then(|| Background::Color(theme.palette().background.weak.color)), + background: lit.then(|| { + Background::Color(theme.palette().primary.weak.color.scale_alpha(0.12)) + }), + ..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() }) .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)) .on_press(Message::Dock(DockMsg::EdgeMenu(Some(side)))) - .style(button::subtle) - .padding([5, 8]); + .style(|theme: &Theme, status| button::Style { + 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()); container(column(items)) .width(Length::Fixed(DOCK_STRIP_W)) @@ -3145,7 +3252,12 @@ impl OpenCADStudio { 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. if let Some((_, gi)) = after.location(id) { 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)); } } + // 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) .width(Fill) .height(Fill) .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> { + 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> = 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> = 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 ─────────────────────────────────────────────────────── diff --git a/src/ui/dock.rs b/src/ui/dock.rs index 4d68739f4..7909afc58 100644 --- a/src/ui/dock.rs +++ b/src/ui/dock.rs @@ -51,8 +51,12 @@ pub enum DockMsg { /// 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). 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), + /// 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), /// Double-click on a docked title bar: float the panel. FloatOut(PanelId), /// 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. pub const DOCK_DIVIDER_W: f32 = 5.0; /// 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. pub const STRIP_CELL_H: f32 = 32.0; -/// Height of the grip bar above each group's icons. -pub const STRIP_GRIP_H: f32 = 10.0; -/// Space above the first group and between groups in the strip. -pub const STRIP_GAP: f32 = 6.0; +/// Space above and below a group's icons, inside the group. +pub const STRIP_PAD: f32 = 6.0; +/// Hairline divider between groups. +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. #[derive(Debug, Clone, Copy, PartialEq)] pub struct StripGroup { - /// Slot index on the edge. + /// Group index on the edge. pub group: usize, - /// Top of the group's grip bar. - pub grip_top: f32, + /// Top of the group's row (just below the divider above it). + pub top: f32, /// Top of the group's first icon. pub icons_top: f32, - /// Number of icons (visible tabs). + /// Number of icons (open pallets). 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. pub fn strip_layout(groups: &[(usize, usize)]) -> (Vec, f32) { - let mut y = STRIP_GAP; + let mut y = 0.0; let placed = groups .iter() .map(|&(group, icons)| { - let grip_top = y; - let icons_top = grip_top + STRIP_GRIP_H; - y = icons_top + icons as f32 * STRIP_CELL_H + STRIP_GAP; - StripGroup { + let g = StripGroup { group, - grip_top, - icons_top, + top: y, + icons_top: y + STRIP_PAD, icons, - } + }; + y = g.bottom() + STRIP_DIVIDER_H; + g }) .collect(); - (placed, y) + (placed, y + STRIP_PLUS_GAP) } /// What a dragged pallet released at height `y` over the icon strip joins. #[derive(Debug, Clone, Copy, PartialEq, Eq)] 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), - /// Group `group`, at position `index` among its visible icons. + /// Group `group`, at position `index` among its open pallets. Tab { group: usize, index: usize }, } -/// Classify a pointer at strip height `y`: over a group's grip (or the gap -/// above it) starts a new group there, over its icons joins it between the -/// nearest icons, below every group starts a new last group. +/// Classify a pointer at strip height `y`: on a divider (or the gap above +/// the first group) starts a new group there, over a group's icons joins it +/// between the nearest icons, below every group starts a new last group. pub fn strip_hit(y: f32, layout: &[StripGroup]) -> StripHit { for g in layout { - if y < g.icons_top { + if y < g.icons_top + STRIP_NEW_GROUP_SLOP { return StripHit::NewGroup(Some(g.group)); } - let bottom = g.icons_top + g.icons as f32 * STRIP_CELL_H + STRIP_GAP * 0.5; - if y < bottom { + if y < g.icons_bottom() - STRIP_NEW_GROUP_SLOP { let index = (((y - g.icons_top) / STRIP_CELL_H).round().max(0.0) as usize) .min(g.icons); return StripHit::Tab { @@ -1211,23 +1233,33 @@ mod tests { } #[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. let (layout, plus_top) = strip_layout(&[(0, 3), (2, 1)]); let g0 = layout[0]; let g1 = layout[1]; - assert_eq!(g0.grip_top, STRIP_GAP); - assert_eq!(g1.icons_top, g1.grip_top + STRIP_GRIP_H); - assert_eq!(plus_top, g1.icons_top + STRIP_CELL_H + STRIP_GAP); - // On the first grip: a new group before slot 0. - assert_eq!(strip_hit(g0.grip_top + 2.0, &layout), StripHit::NewGroup(Some(0))); + assert_eq!(g0.icons_top, STRIP_PAD); + assert_eq!(g1.top, g0.bottom() + STRIP_DIVIDER_H); + assert_eq!(plus_top, g1.bottom() + STRIP_DIVIDER_H + STRIP_PLUS_GAP); + // Above the first icon: a new group before group 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. assert_eq!( strip_hit(g0.icons_top + STRIP_CELL_H + 4.0, &layout), StripHit::Tab { group: 0, index: 1 } ); - // On the second group's grip: a new group before slot 2. - assert_eq!(strip_hit(g1.grip_top + 1.0, &layout), StripHit::NewGroup(Some(2))); + // Lower half of the last icon: into group 0 at the end. + 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. assert_eq!(strip_hit(plus_top + 20.0, &layout), StripHit::NewGroup(None)); }