diff --git a/assets/icons/ui/pin_active.svg b/assets/icons/ui/pin_active.svg
new file mode 100644
index 000000000..3a291545c
--- /dev/null
+++ b/assets/icons/ui/pin_active.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/ui/resize_left.svg b/assets/icons/ui/resize_left.svg
new file mode 100644
index 000000000..bacde830e
--- /dev/null
+++ b/assets/icons/ui/resize_left.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 8f79a88fd..48cb2fd26 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -863,8 +863,11 @@ pub(super) struct OpenCADStudio {
pub(crate) dock_grab_offset: iced::Vector,
/// Splitter being dragged between two slots on a side (upper, lower).
pub(crate) dock_split_drag: Option<(crate::app::config::DockSide, usize, usize)>,
- /// Floating panel being resized from its corner grip.
- pub(crate) dock_float_resizing: Option,
+ /// Floating panel being resized from its corner grip, and whether from
+ /// the bottom-left corner.
+ pub(crate) dock_float_resizing: Option<(crate::ui::dock::PanelId, bool)>,
+ /// Docked panel whose title bar the pointer is over (shows pin / close).
+ pub(crate) dock_title_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,
@@ -4304,6 +4307,7 @@ impl OpenCADStudio {
dock_grab_offset: iced::Vector::new(0.0, 0.0),
dock_split_drag: None,
dock_float_resizing: None,
+ dock_title_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 b5d9b8578..3aeb66db5 100644
--- a/src/app/update/dialog.rs
+++ b/src/app/update/dialog.rs
@@ -580,14 +580,18 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
self.save_config();
iced::Task::none()
}
- DockMsg::FloatResizeGrab(id) => {
+ DockMsg::FloatResizeGrab(id, from_left) => {
self.dock.raise_float(id);
- self.dock_float_resizing = Some(id);
+ self.dock_float_resizing = Some((id, from_left));
self.dock_dragging = None;
self.dock_drag_last = None;
self.dock_drag_target = None;
iced::Task::none()
}
+ DockMsg::TitleHover(id) => {
+ self.dock_title_hover = id;
+ iced::Task::none()
+ }
DockMsg::FloatRaise(id) => {
self.dock.raise_float(id);
iced::Task::none()
@@ -707,12 +711,14 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
);
}
}
- } else if let Some(id) = self.dock_float_resizing {
+ } else if let Some((id, from_left)) = self.dock_float_resizing {
if let (Some(last), Some(f)) = (self.dock_drag_last, self.dock.float_rect(id)) {
+ let dx = point.x - last.x;
self.dock.resize_float(
id,
- f.w + point.x - last.x,
+ if from_left { f.w - dx } else { f.w + dx },
f.h + point.y - last.y,
+ from_left,
);
}
} else if let Some(id) = self.dock_resizing {
@@ -2042,12 +2048,19 @@ mod tests {
w: 260.0,
h: 300.0,
});
- let _ = app.on_dock(DockMsg::FloatResizeGrab(PanelId::BlockPalette));
+ let _ = app.on_dock(DockMsg::FloatResizeGrab(PanelId::BlockPalette, false));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(360.0, 400.0)));
let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(400.0, 450.0)));
let _ = app.on_dock(DockMsg::DragRelease);
let f = app.dock.float_rect(PanelId::BlockPalette).unwrap();
assert_eq!((f.w, f.h), (300.0, 350.0));
+ // The bottom-left grip grows the panel leftward, keeping its right edge.
+ let _ = app.on_dock(DockMsg::FloatResizeGrab(PanelId::BlockPalette, true));
+ let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(100.0, 450.0)));
+ let _ = app.on_dock(DockMsg::DragMove(iced::Point::new(60.0, 450.0)));
+ let _ = app.on_dock(DockMsg::DragRelease);
+ let g = app.dock.float_rect(PanelId::BlockPalette).unwrap();
+ assert_eq!((g.x, g.w, g.h), (f.x - 40.0, 340.0, 350.0));
}
#[test]
diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs
index 294311ca6..d2cedd212 100644
--- a/src/app/view/mod.rs
+++ b/src/app/view/mod.rs
@@ -1930,7 +1930,9 @@ bg={bg_ms:.1}ms n={view_count}"
// Floating panels sit over the workspace, back to front.
let mut layers: Vec> = vec![workspace];
for f in &self.dock.floating {
- if self.dock_panel_visible(f.id) {
+ // Once a drag is under way only its preview shows the panel.
+ let dragged = self.dock_dragging == Some(f.id) && self.dock_drag_target.is_some();
+ if self.dock_panel_visible(f.id) && !dragged {
layers.push(self.floating_panel(*f, tab));
}
}
@@ -1956,8 +1958,12 @@ bg={bg_ms:.1}ms n={view_count}"
mouse_area(workspace)
.on_move(move |p| Message::Dock(crate::ui::dock::DockMsg::DragMove(p)))
.on_release(Message::Dock(crate::ui::dock::DockMsg::DragRelease))
- .interaction(if self.dock_float_resizing.is_some() {
- iced::mouse::Interaction::ResizingDiagonallyDown
+ .interaction(if let Some((_, from_left)) = self.dock_float_resizing {
+ if from_left {
+ iced::mouse::Interaction::ResizingDiagonallyUp
+ } else {
+ iced::mouse::Interaction::ResizingDiagonallyDown
+ }
} else if any_resizing {
iced::mouse::Interaction::ResizingHorizontally
} else if any_splitting {
@@ -2897,6 +2903,7 @@ impl OpenCADStudio {
let chrome = crate::ui::dock::Chrome {
auto_collapse: self.dock.auto_collapse(id),
floating,
+ title_hovered: self.dock_title_hover == Some(id),
};
match id {
crate::ui::dock::PanelId::Properties => tab.properties.view(width, chrome),
@@ -2978,25 +2985,48 @@ impl OpenCADStudio {
) -> Element<'a, Message> {
use crate::ui::dock::DockMsg;
let (ww, wh) = self.dock_workspace_size();
- let bar_left = f.x + (f.w + DOCK_FLOAT_BAR_W) * 0.5 < ww * 0.5;
+ // The bar faces the nearer workspace edge; while resizing, it stays
+ // opposite the grip being dragged so it cannot flip mid-drag.
+ let bar_left = match self.dock_float_resizing {
+ Some((id, from_left)) if id == f.id => !from_left,
+ _ => f.x + (f.w + DOCK_FLOAT_BAR_W) * 0.5 < ww * 0.5,
+ };
let hidden = self.dock.auto_collapse(f.id) && self.dock_expanded != Some(f.id);
let bar = floating_title_bar(f.id, self.dock.auto_collapse(f.id), bar_left);
let content: Element<'_, Message> = if hidden {
bar
} else {
+ // The resize grip sits in the bottom corner away from the bar.
+ let grip_left = !bar_left;
+ let grip_icon = crate::ui::icons::themed_secondary(
+ if grip_left {
+ crate::ui::icons::RESIZE_LEFT
+ } else {
+ crate::ui::icons::RESIZE
+ },
+ 12.0,
+ );
let grip = mouse_area(
- container(crate::ui::icons::themed_secondary(crate::ui::icons::RESIZE, 12.0))
+ container(grip_icon)
.center_x(Length::Fixed(14.0))
.center_y(Length::Fixed(14.0)),
)
- .on_press(Message::Dock(DockMsg::FloatResizeGrab(f.id)))
- .interaction(iced::mouse::Interaction::ResizingDiagonallyDown);
+ .on_press(Message::Dock(DockMsg::FloatResizeGrab(f.id, grip_left)))
+ .interaction(if grip_left {
+ iced::mouse::Interaction::ResizingDiagonallyUp
+ } else {
+ iced::mouse::Interaction::ResizingDiagonallyDown
+ });
let body = stack![
self.panel_body(f.id, f.w, true, tab),
container(grip)
.width(Fill)
.height(Fill)
- .align_x(iced::alignment::Horizontal::Right)
+ .align_x(if grip_left {
+ iced::alignment::Horizontal::Left
+ } else {
+ iced::alignment::Horizontal::Right
+ })
.align_y(iced::alignment::Vertical::Bottom),
]
.width(Length::Fixed(f.w))
@@ -3562,49 +3592,13 @@ fn floating_title_bar(
on_left: bool,
) -> Element<'static, Message> {
use crate::ui::dock::DockMsg;
- let close = button(crate::ui::icons::themed_secondary(crate::ui::icons::CLOSE, 12.0))
- .on_press(Message::Dock(DockMsg::Close(id)))
- .style(button::subtle)
- .padding([3, 5]);
- let close = iced::widget::tooltip(
- close,
- text(t!("Close")).size(10),
- if on_left {
- iced::widget::tooltip::Position::Left
- } else {
- iced::widget::tooltip::Position::Right
- },
- )
- .gap(4);
- let pin_icon = if auto_collapse {
- crate::ui::icons::themed_primary_weak_text(crate::ui::icons::PIN, 12.0)
+ let tip = if on_left {
+ iced::widget::tooltip::Position::Left
} else {
- crate::ui::icons::themed_secondary(crate::ui::icons::PIN, 12.0)
+ iced::widget::tooltip::Position::Right
};
- let pin = button(pin_icon)
- .on_press(Message::Dock(DockMsg::AutoCollapseToggle(id)))
- .style(move |theme: &Theme, status| {
- let mut style = button::subtle(theme, status);
- if auto_collapse {
- let palette = theme.palette();
- style.background = Some(Background::Color(palette.primary.weak.color));
- style.text_color = palette.primary.weak.text;
- style.border.color = palette.primary.base.color;
- style.border.width = 1.0;
- }
- style
- })
- .padding([3, 5]);
- let pin = iced::widget::tooltip(
- pin,
- text(t!("Auto-hide")).size(10),
- if on_left {
- iced::widget::tooltip::Position::Left
- } else {
- iced::widget::tooltip::Position::Right
- },
- )
- .gap(4);
+ let close = crate::ui::dock::close_button(id, tip);
+ let pin = crate::ui::dock::pin_button(id, auto_collapse, tip);
let title = canvas(VBarLabel {
text: id.title().to_string(),
clockwise: false,
diff --git a/src/ui/dock.rs b/src/ui/dock.rs
index d3cb445a8..c891011e8 100644
--- a/src/ui/dock.rs
+++ b/src/ui/dock.rs
@@ -38,8 +38,11 @@ pub enum DockMsg {
SplitGrab(DockSide, usize, usize),
/// Give every slot on a side the same height again.
SplitReset(DockSide),
- /// Begin resizing floating `panel` from its corner grip.
- FloatResizeGrab(PanelId),
+ /// Begin resizing floating `panel` from its corner grip; `true` for the
+ /// bottom-left grip (a panel whose title bar is on the right).
+ FloatResizeGrab(PanelId, bool),
+ /// The pointer entered (`Some`) or left (`None`) a docked title bar.
+ TitleHover(Option),
/// Bring floating `panel` to the front.
FloatRaise(PanelId),
/// The pointer left the edge column; collapse any auto-collapsing panel.
@@ -454,10 +457,15 @@ impl DockState {
}
}
- /// Resize a floating panel, keeping it at least a usable size.
- pub fn resize_float(&mut self, id: PanelId, w: f32, h: f32) {
+ /// Resize a floating panel, keeping it at least a usable size. With
+ /// `keep_right` the right edge stays put (resizing from the left corner).
+ pub fn resize_float(&mut self, id: PanelId, w: f32, h: f32, keep_right: bool) {
if let Some(f) = self.floating.iter_mut().find(|f| f.id == id) {
- f.w = w.clamp(DOCK_MIN_W, id.max_width());
+ let new_w = w.clamp(DOCK_MIN_W, id.max_width());
+ if keep_right {
+ f.x += f.w - new_w;
+ }
+ f.w = new_w;
f.h = h.max(FLOAT_MIN_H);
}
}
@@ -582,6 +590,8 @@ pub struct Chrome {
/// The panel floats; its title lives in the vertical side bar drawn by
/// the floating frame instead of the horizontal title bar.
pub floating: bool,
+ /// The pointer is over the title bar, which shows its pin and close.
+ pub title_hovered: bool,
}
/// The row every docked panel starts with: its title, the auto-collapse pin
@@ -591,48 +601,65 @@ pub fn title_bar<'a>(id: PanelId, title: String, chrome: Chrome) -> Element<'a,
if chrome.floating {
return Space::new().width(0).height(0).into();
}
- let auto_collapse = chrome.auto_collapse;
- let pin_icon = if auto_collapse {
- crate::ui::icons::themed_primary_weak_text(crate::ui::icons::PIN, 12.0)
- } else {
- crate::ui::icons::themed_secondary(crate::ui::icons::PIN, 12.0)
- };
- let pin = button(pin_icon)
+ // The pin and close only show while the pointer is over the bar; the
+ // fixed height keeps the bar from jumping when they appear.
+ let mut bar = row![text(title).size(12), Space::new().width(Length::Fill)]
+ .spacing(3)
+ .height(Length::Fixed(TITLE_BUTTON_H))
+ .align_y(iced::Center);
+ if chrome.title_hovered {
+ bar = bar
+ .push(pin_button(id, chrome.auto_collapse, tooltip::Position::Bottom))
+ .push(close_button(id, tooltip::Position::Bottom));
+ }
+ mouse_area(
+ container(bar)
+ .style(|theme: &Theme| container::Style {
+ background: Some(Background::Color(theme.palette().background.weak.color)),
+ ..Default::default()
+ })
+ .width(Length::Fill)
+ .padding([3, 6]),
+ )
+ .on_press(Message::Dock(DockMsg::DockGrab(id)))
+ .on_enter(Message::Dock(DockMsg::TitleHover(Some(id))))
+ .on_exit(Message::Dock(DockMsg::TitleHover(None)))
+ .interaction(iced::mouse::Interaction::Grab)
+ .into()
+}
+
+/// Height of a title bar's button row.
+const TITLE_BUTTON_H: f32 = 20.0;
+
+/// The auto-hide pin. While auto-hide is on the pin is drawn tilted instead
+/// of on a coloured background.
+pub fn pin_button<'a>(id: PanelId, auto_collapse: bool, tip: tooltip::Position) -> Element<'a, Message> {
+ let icon = crate::ui::icons::themed_secondary(
+ if auto_collapse {
+ crate::ui::icons::PIN_ACTIVE
+ } else {
+ crate::ui::icons::PIN
+ },
+ 12.0,
+ );
+ let pin = button(icon)
.on_press(Message::Dock(DockMsg::AutoCollapseToggle(id)))
- .style(move |theme: &Theme, status| {
- let mut style = button::subtle(theme, status);
- if auto_collapse {
- let palette = theme.palette();
- style.background = Some(Background::Color(palette.primary.weak.color));
- style.text_color = palette.primary.weak.text;
- style.border.color = palette.primary.base.color;
- style.border.width = 1.0;
- }
- style
- })
+ .style(button::subtle)
.padding([3, 5]);
- let pin = tooltip(pin, text(crate::t!("Auto")).size(10), tooltip::Position::Bottom).gap(4);
+ tooltip(pin, text(crate::t!("Auto-hide")).size(10), tip)
+ .gap(4)
+ .into()
+}
+
+/// The close button of a panel's title bar.
+pub fn close_button<'a>(id: PanelId, tip: tooltip::Position) -> Element<'a, Message> {
let close = button(crate::ui::icons::themed_secondary(crate::ui::icons::CLOSE, 12.0))
.on_press(Message::Dock(DockMsg::Close(id)))
.style(button::subtle)
.padding([3, 5]);
- let close = tooltip(close, text(crate::t!("Close")).size(10), tooltip::Position::Bottom).gap(4);
- mouse_area(
- container(
- row![text(title).size(12), Space::new().width(Length::Fill), pin, close]
- .spacing(3)
- .align_y(iced::Center),
- )
- .style(|theme: &Theme| container::Style {
- background: Some(Background::Color(theme.palette().background.weak.color)),
- ..Default::default()
- })
- .width(Length::Fill)
- .padding([3, 6]),
- )
- .on_press(Message::Dock(DockMsg::DockGrab(id)))
- .interaction(iced::mouse::Interaction::Grab)
- .into()
+ tooltip(close, text(crate::t!("Close")).size(10), tip)
+ .gap(4)
+ .into()
}
/// Side length of a panel toolbar button's icon.
@@ -901,9 +928,13 @@ mod tests {
w: 300.0,
h: 300.0,
});
- state.resize_float(PanelId::Count, 10.0, 10.0);
+ state.resize_float(PanelId::Count, 10.0, 10.0, false);
let f = state.float_rect(PanelId::Count).unwrap();
assert_eq!((f.w, f.h), (DOCK_MIN_W, FLOAT_MIN_H));
+ // From the left corner the right edge stays where it was.
+ state.resize_float(PanelId::Count, 350.0, 300.0, true);
+ let f = state.float_rect(PanelId::Count).unwrap();
+ assert_eq!((f.x, f.w), (DOCK_MIN_W - 350.0, 350.0));
}
#[test]
@@ -970,8 +1001,8 @@ mod tests {
#[test]
fn floating_title_bar_replaces_the_horizontal_one() {
let floating = Chrome {
- auto_collapse: false,
floating: true,
+ ..Default::default()
};
let bar: Element<'_, Message> = title_bar(PanelId::Count, "Count".into(), floating);
assert_eq!(
diff --git a/src/ui/icons.rs b/src/ui/icons.rs
index 694b98a07..2330bd179 100644
--- a/src/ui/icons.rs
+++ b/src/ui/icons.rs
@@ -72,7 +72,11 @@ pub static COPY: &[u8] = include_bytes!("../../assets/icons/ui/copy.svg");
pub static MENU: &[u8] = include_bytes!("../../assets/icons/ui/menu.svg");
pub static MOVE: &[u8] = include_bytes!("../../assets/icons/ui/move.svg");
pub static RESIZE: &[u8] = include_bytes!("../../assets/icons/ui/resize.svg");
+/// `RESIZE` mirrored for a bottom-left corner grip.
+pub static RESIZE_LEFT: &[u8] = include_bytes!("../../assets/icons/ui/resize_left.svg");
pub static PIN: &[u8] = include_bytes!("../../assets/icons/ui/pin.svg");
+/// `PIN` tilted 30°: the pin while auto-hide is on.
+pub static PIN_ACTIVE: &[u8] = include_bytes!("../../assets/icons/ui/pin_active.svg");
pub static SPLIT_V: &[u8] = include_bytes!("../../assets/icons/ui/split_v.svg");
pub static SPLIT_H: &[u8] = include_bytes!("../../assets/icons/ui/split_h.svg");
pub static GRID: &[u8] = include_bytes!("../../assets/icons/ui/grid.svg");