forked from 1b-agent/opencadstudio-upstream
feat(ui): anchor every ribbon dropdown and status-bar popup to its widget, flipping at window edges
All ribbon dropdowns (tool dropdowns, undo/redo history, and the layer / colour / linetype / lineweight / style combos) and all status-bar popups (scale, units, OSNAP, isolate, filter, customization menu, layout list) now open directly at their widget instead of at a layout-estimated position, and flip to the other side when the panel would cross a window edge. Each dropdown/pill widget is wrapped in a transparent PosReport that records its screen bounds into a thread-local map on draw. The overlays read those bounds: - Ribbon: Ribbon::dd_anchor(id, panel_w, win_w) returns (align_right, h_pad, top); position_ribbon_dropdown places the panel below the widget, flipping right-aligned near the right edge. - Status bar: popup builders take (pill_bounds, win_size) and call popup::position_statusbar_popup / popup_anchor, opening just above the pill and flipping left/right at the window edge. The window size is now tracked (win_size, set on WindowResized). The old layout estimators compute_dropdown_left / compute_layer_combo_left / compute_prop_combo_left / compute_history_dropdown_left and osnap_popup_right_offset are deleted. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
56e48b8d0f
commit
39043e6e56
13 files changed
+310
-383
No files matched your search
@@ -4,7 +4,7 @@
|
||||
//! in the viewport right-click menu.
|
||||
|
||||
use iced::widget::{button, column, container, mouse_area, row, text};
|
||||
use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
|
||||
use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme};
|
||||
|
||||
use crate::app::Message;
|
||||
|
||||
@@ -13,7 +13,12 @@ use crate::app::Message;
|
||||
///
|
||||
/// - `has_selection`: enables Isolate / Hide (they act on the selection).
|
||||
/// - `isolation_active`: enables End Isolation (something is hidden).
|
||||
pub fn isolate_popup_overlay(has_selection: bool, isolation_active: bool) -> Element<'static, Message> {
|
||||
pub fn isolate_popup_overlay(
|
||||
has_selection: bool,
|
||||
isolation_active: bool,
|
||||
pill: Option<Rectangle>,
|
||||
win: (f32, f32),
|
||||
) -> Element<'static, Message> {
|
||||
let rows = column![
|
||||
action_row(
|
||||
"Isolate Objects",
|
||||
@@ -44,17 +49,7 @@ pub fn isolate_popup_overlay(has_selection: bool, isolation_active: bool) -> Ele
|
||||
})
|
||||
.width(Length::Fixed(160.0));
|
||||
|
||||
let positioned = container(panel)
|
||||
.align_right(Fill)
|
||||
.align_bottom(Fill)
|
||||
.padding(Padding {
|
||||
bottom: 27.0,
|
||||
right: 4.0,
|
||||
top: 0.0,
|
||||
left: 0.0,
|
||||
})
|
||||
.width(Fill)
|
||||
.height(Fill);
|
||||
let positioned = super::position_statusbar_popup(panel.into(), pill, win, 160.0, true);
|
||||
|
||||
mouse_area(positioned)
|
||||
.on_press(Message::CloseIsolatePopup)
|
||||
|
||||
@@ -4,3 +4,76 @@ pub mod scale_popup;
|
||||
pub mod selection_filter_popup;
|
||||
pub mod snap_popup;
|
||||
pub mod units_popup;
|
||||
|
||||
use iced::widget::container;
|
||||
use iced::{Element, Fill, Padding, Rectangle};
|
||||
|
||||
use crate::app::Message;
|
||||
|
||||
/// Decide a status-bar popup's horizontal anchor and paddings from its pill's
|
||||
/// screen bounds. Returns `(align_right, horizontal_pad, bottom_pad)`.
|
||||
///
|
||||
/// The popup opens just above the pill. Horizontally it prefers `prefer_right`
|
||||
/// (right edge aligned to the pill's right, growing left) but flips to the other
|
||||
/// side when that direction would cross the window edge.
|
||||
pub fn popup_anchor(
|
||||
pill: Option<Rectangle>,
|
||||
win: (f32, f32),
|
||||
popup_w: f32,
|
||||
prefer_right: bool,
|
||||
) -> (bool, f32, f32) {
|
||||
let Some(b) = pill else {
|
||||
// No recorded position yet — fall back to the bottom-right corner.
|
||||
return (prefer_right, 4.0, 27.0);
|
||||
};
|
||||
let bottom = (win.1 - b.y).max(0.0);
|
||||
let right_pad = (win.0 - (b.x + b.width)).max(4.0);
|
||||
let left_pad = b.x.max(4.0);
|
||||
|
||||
if prefer_right {
|
||||
// Right-aligned popup grows left; flip left-aligned if it would pass the
|
||||
// left window edge.
|
||||
if (b.x + b.width) - popup_w < 2.0 {
|
||||
(false, left_pad, bottom)
|
||||
} else {
|
||||
(true, right_pad, bottom)
|
||||
}
|
||||
} else {
|
||||
// Left-aligned popup grows right; flip right-aligned if it would pass the
|
||||
// right window edge.
|
||||
if b.x + popup_w > win.0 - 2.0 {
|
||||
(true, right_pad, bottom)
|
||||
} else {
|
||||
(false, left_pad, bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrap a status-bar popup `panel` in a full-window container positioned just
|
||||
/// above its pill, flipping horizontally to stay on screen.
|
||||
pub fn position_statusbar_popup<'a>(
|
||||
panel: Element<'a, Message>,
|
||||
pill: Option<Rectangle>,
|
||||
win: (f32, f32),
|
||||
popup_w: f32,
|
||||
prefer_right: bool,
|
||||
) -> Element<'a, Message> {
|
||||
let (align_right, h_pad, bottom) = popup_anchor(pill, win, popup_w, prefer_right);
|
||||
let pad = Padding {
|
||||
top: 0.0,
|
||||
bottom,
|
||||
left: if align_right { 0.0 } else { h_pad },
|
||||
right: if align_right { h_pad } else { 0.0 },
|
||||
};
|
||||
let c = container(panel)
|
||||
.align_bottom(Fill)
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.padding(pad);
|
||||
let c = if align_right {
|
||||
c.align_right(Fill)
|
||||
} else {
|
||||
c.align_left(Fill)
|
||||
};
|
||||
c.into()
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
//! Rendered as a floating overlay above the status bar, same pattern as snap_popup.
|
||||
|
||||
use iced::widget::{button, column, container, mouse_area, row, text};
|
||||
use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
|
||||
use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme};
|
||||
|
||||
use crate::app::Message;
|
||||
|
||||
@@ -20,6 +20,8 @@ pub fn scale_popup_overlay(
|
||||
current_anno_scale: f32,
|
||||
viewport_scale: Option<f64>,
|
||||
file_scales: Vec<(String, f32, f64)>,
|
||||
pill: Option<Rectangle>,
|
||||
win: (f32, f32),
|
||||
) -> Element<'static, Message> {
|
||||
let rows: Vec<Element<'static, Message>> = file_scales
|
||||
.into_iter()
|
||||
@@ -52,17 +54,7 @@ pub fn scale_popup_overlay(
|
||||
})
|
||||
.width(Length::Fixed(120.0));
|
||||
|
||||
let positioned = container(panel)
|
||||
.align_right(Fill)
|
||||
.align_bottom(Fill)
|
||||
.padding(Padding {
|
||||
bottom: 27.0,
|
||||
right: 4.0,
|
||||
top: 0.0,
|
||||
left: 0.0,
|
||||
})
|
||||
.width(Fill)
|
||||
.height(Fill);
|
||||
let positioned = super::position_statusbar_popup(panel.into(), pill, win, 120.0, true);
|
||||
|
||||
mouse_area(positioned)
|
||||
.on_press(Message::CloseScalePopup)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
use rustc_hash::FxHashSet as HashSet;
|
||||
|
||||
use iced::widget::{button, column, container, mouse_area, row, text};
|
||||
use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
|
||||
use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme};
|
||||
|
||||
use crate::app::Message;
|
||||
|
||||
@@ -17,6 +17,8 @@ use crate::app::Message;
|
||||
pub fn selection_filter_popup_overlay(
|
||||
types: Vec<String>,
|
||||
excluded: &HashSet<String>,
|
||||
pill: Option<Rectangle>,
|
||||
win: (f32, f32),
|
||||
) -> Element<'static, Message> {
|
||||
// "Select All / Clear All" header, mirroring the OSNAP popup: Select All
|
||||
// clears every exclusion, Clear All excludes every present type.
|
||||
@@ -70,17 +72,7 @@ pub fn selection_filter_popup_overlay(
|
||||
})
|
||||
.width(Length::Fixed(180.0));
|
||||
|
||||
let positioned = container(panel)
|
||||
.align_right(Fill)
|
||||
.align_bottom(Fill)
|
||||
.padding(Padding {
|
||||
bottom: 27.0,
|
||||
right: 4.0,
|
||||
top: 0.0,
|
||||
left: 0.0,
|
||||
})
|
||||
.width(Fill)
|
||||
.height(Fill);
|
||||
let positioned = super::position_statusbar_popup(panel.into(), pill, win, 180.0, true);
|
||||
|
||||
mouse_area(positioned)
|
||||
.on_press(Message::CloseSelectionFilterPopup)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! inserted into the view stack when `snap_popup_open` is true.
|
||||
|
||||
use iced::widget::{button, column, container, mouse_area, row, text};
|
||||
use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
|
||||
use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme};
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::snap::{SnapType, Snapper, ALL_SNAP_MODES};
|
||||
@@ -12,7 +12,11 @@ use crate::snap::{SnapType, Snapper, ALL_SNAP_MODES};
|
||||
/// Returns a full-screen overlay element:
|
||||
/// - a transparent click-catcher that closes the popup on outside click
|
||||
/// - the popup panel pinned to the bottom-right (above the status bar)
|
||||
pub fn snap_popup_overlay<'a>(snapper: &'a Snapper, right_offset: f32) -> Element<'a, Message> {
|
||||
pub fn snap_popup_overlay<'a>(
|
||||
snapper: &'a Snapper,
|
||||
pill: Option<Rectangle>,
|
||||
win: (f32, f32),
|
||||
) -> Element<'a, Message> {
|
||||
// ── Panel content ─────────────────────────────────────────────────────
|
||||
let all_on = snapper.all_on();
|
||||
let none_on = snapper.none_on();
|
||||
@@ -55,23 +59,8 @@ pub fn snap_popup_overlay<'a>(snapper: &'a Snapper, right_offset: f32) -> Elemen
|
||||
})
|
||||
.width(Length::Fixed(PANEL_W));
|
||||
|
||||
// ── Position above the status bar, anchored by its bottom-LEFT corner at
|
||||
// `right_offset` from the window's right edge (the Osnap button), so it
|
||||
// opens up and to the right of the button. `align_right` positions by the
|
||||
// panel's right edge, so pad by the anchor minus the panel width; clamp so
|
||||
// a narrow bar can't push it off-screen. (#248)
|
||||
let right = (right_offset - PANEL_W).max(4.0);
|
||||
let positioned = container(panel)
|
||||
.align_right(Fill)
|
||||
.align_bottom(Fill)
|
||||
.padding(Padding {
|
||||
bottom: 27.0,
|
||||
right,
|
||||
top: 0.0,
|
||||
left: 0.0,
|
||||
})
|
||||
.width(Fill)
|
||||
.height(Fill);
|
||||
// Anchored just above its pill, flipping horizontally to stay on screen.
|
||||
let positioned = super::position_statusbar_popup(panel.into(), pill, win, PANEL_W, true);
|
||||
|
||||
// ── Click-catcher: closes popup on any outside click ──────────────────
|
||||
mouse_area(positioned)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! floating overlay above the status bar, same pattern as the scale picker.
|
||||
|
||||
use iced::widget::{button, column, container, mouse_area, row, text};
|
||||
use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
|
||||
use iced::{Background, Border, Color, Element, Fill, Length, Rectangle, Theme};
|
||||
|
||||
use crate::app::Message;
|
||||
|
||||
@@ -37,7 +37,11 @@ pub fn unit_short(code: i16) -> &'static str {
|
||||
|
||||
/// Full-screen overlay: transparent click-catcher + units list pinned
|
||||
/// bottom-right, above the status bar.
|
||||
pub fn units_popup_overlay(current: i16) -> Element<'static, Message> {
|
||||
pub fn units_popup_overlay(
|
||||
current: i16,
|
||||
pill: Option<Rectangle>,
|
||||
win: (f32, f32),
|
||||
) -> Element<'static, Message> {
|
||||
let rows: Vec<Element<'static, Message>> = UNITS
|
||||
.iter()
|
||||
.map(|&(code, label)| unit_row(label, code == current, Message::SetDrawingUnits(code)))
|
||||
@@ -55,17 +59,7 @@ pub fn units_popup_overlay(current: i16) -> Element<'static, Message> {
|
||||
})
|
||||
.width(Length::Fixed(140.0));
|
||||
|
||||
let positioned = container(panel)
|
||||
.align_right(Fill)
|
||||
.align_bottom(Fill)
|
||||
.padding(Padding {
|
||||
bottom: 27.0,
|
||||
right: 4.0,
|
||||
top: 0.0,
|
||||
left: 0.0,
|
||||
})
|
||||
.width(Fill)
|
||||
.height(Fill);
|
||||
let positioned = super::position_statusbar_popup(panel.into(), pill, win, 140.0, true);
|
||||
|
||||
mouse_area(positioned)
|
||||
.on_press(Message::CloseUnitsPopup)
|
||||
|
||||
Reference in new issue
Block a user