forked from 1b-agent/opencadstudio-upstream
feat(ribbon): collapse overflowing panels to flyout buttons, anchor dropdowns to their widget
Third stage of the adaptive tool area (shrink -> compact -> collapse). When even the compact panels don't fit, DensitySwap picks a new CollapsePanels widget that keeps a left prefix of panels inline and collapses the rest to buttons; clicking a button opens the full panel as an overlay flyout anchored below it (closes on outside press, mirroring color_select's Floating). A collapsed panel's button shows a live representative tool — the panel's last-used tool (tracked per panel in Ribbon::note_panel_tool on every tool click), defaulting to its first — rendered as a large icon. To support this, render_small now also renders Large* items small and render_large now renders plain Tool/Dropdown items large. Large dropdowns render as icon + label + a bottom arrow strip via a shared render_large_dropdown helper, used in both the full ribbon and collapsed panels. Ribbon dropdowns (tool dropdowns + undo/redo history) now anchor their overlay directly below their widget at any size: a transparent PosReport wrapper records each dropdown button's screen bounds into a thread-local map on draw, and the overlays read it via Ribbon::dd_anchor instead of the layout-estimating compute_*_left helpers. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
ff36a1bdac
commit
56e48b8d0f
7 files changed
+1024
-194
No files matched your search
@@ -1403,6 +1403,8 @@ pub enum Message {
|
||||
SnapClearAll,
|
||||
/// Toggle a ribbon dropdown open/closed.
|
||||
ToggleRibbonDropdown(String),
|
||||
/// Toggle a collapsed ribbon panel's flyout open/closed (by panel title).
|
||||
ToggleRibbonPanel(String),
|
||||
/// Close any open ribbon dropdown (click-catcher outside the panel).
|
||||
CloseRibbonDropdown,
|
||||
/// User selected a specific item from a ribbon dropdown.
|
||||
|
||||
@@ -96,6 +96,10 @@ impl OpenCADStudio {
|
||||
|
||||
|
||||
pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEvent) -> Task<Message> {
|
||||
// Dismiss any open dropdown / collapsed-panel flyout on tool use,
|
||||
// and remember this tool as its panel's last-used one.
|
||||
self.ribbon.close_dropdown();
|
||||
self.ribbon.note_panel_tool(&tool_id);
|
||||
self.ribbon.activate_tool(&tool_id);
|
||||
match event {
|
||||
ModuleEvent::Command(cmd) => return self.dispatch_command(&cmd),
|
||||
|
||||
@@ -1655,6 +1655,10 @@ impl OpenCADStudio {
|
||||
self.ribbon.toggle_dropdown(&id);
|
||||
Task::none()
|
||||
}
|
||||
Message::ToggleRibbonPanel(id) => {
|
||||
self.ribbon.toggle_collapsed_panel(&id);
|
||||
Task::none()
|
||||
}
|
||||
Message::CloseRibbonDropdown => {
|
||||
self.ribbon.close_dropdown();
|
||||
Task::none()
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
//! Collapsing ribbon panels.
|
||||
//!
|
||||
//! Given a list of panels (each with an inline element, a collapsed title
|
||||
//! button, and a full flyout), this widget lays out as many inline panels as
|
||||
//! fit the width from the left and collapses the rest into their title buttons.
|
||||
//! Clicking a collapsed button opens that panel's flyout as an overlay anchored
|
||||
//! just below the button.
|
||||
|
||||
use std::cell::Cell;
|
||||
|
||||
use iced::advanced::layout::{self, Layout};
|
||||
use iced::advanced::widget::{self, Widget};
|
||||
use iced::advanced::{mouse, overlay, renderer, Clipboard, Shell};
|
||||
use iced::{Element, Event, Length, Point, Rectangle, Renderer, Size, Theme, Vector};
|
||||
|
||||
use crate::app::Message;
|
||||
|
||||
/// One ribbon panel in its three renderings.
|
||||
pub struct Panel<'a> {
|
||||
pub id: String,
|
||||
pub inline: Element<'a, Message>,
|
||||
pub button: Element<'a, Message>,
|
||||
pub flyout: Element<'a, Message>,
|
||||
}
|
||||
|
||||
pub struct CollapsePanels<'a> {
|
||||
panels: Vec<Panel<'a>>,
|
||||
/// Title of the panel whose flyout is open (if any).
|
||||
open: Option<String>,
|
||||
/// Row height (a full ribbon tool-area height).
|
||||
row_h: f32,
|
||||
/// Number of panels shown inline from the left; set during layout.
|
||||
split: Cell<usize>,
|
||||
}
|
||||
|
||||
impl<'a> CollapsePanels<'a> {
|
||||
pub fn new(panels: Vec<Panel<'a>>, open: Option<String>, row_h: f32) -> Self {
|
||||
Self {
|
||||
panels,
|
||||
open,
|
||||
row_h,
|
||||
split: Cell::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Element used inline for panel `i` given the split `k`.
|
||||
fn shown(&self, i: usize, k: usize) -> &Element<'a, Message> {
|
||||
if i < k {
|
||||
&self.panels[i].inline
|
||||
} else {
|
||||
&self.panels[i].button
|
||||
}
|
||||
}
|
||||
|
||||
fn shown_mut(&mut self, i: usize, k: usize) -> &mut Element<'a, Message> {
|
||||
if i < k {
|
||||
&mut self.panels[i].inline
|
||||
} else {
|
||||
&mut self.panels[i].button
|
||||
}
|
||||
}
|
||||
|
||||
/// Tree index of the element shown inline for panel `i` given split `k`.
|
||||
fn shown_tree(i: usize, k: usize) -> usize {
|
||||
// trees are laid out [inline_0, button_0, flyout_0, inline_1, …]
|
||||
if i < k {
|
||||
3 * i
|
||||
} else {
|
||||
3 * i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
|
||||
fn children(&self) -> Vec<widget::Tree> {
|
||||
let mut v = Vec::with_capacity(self.panels.len() * 3);
|
||||
for p in &self.panels {
|
||||
v.push(widget::Tree::new(&p.inline));
|
||||
v.push(widget::Tree::new(&p.button));
|
||||
v.push(widget::Tree::new(&p.flyout));
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut widget::Tree) {
|
||||
let mut refs: Vec<&dyn Widget<Message, Theme, Renderer>> = Vec::new();
|
||||
for p in &self.panels {
|
||||
refs.push(p.inline.as_widget());
|
||||
refs.push(p.button.as_widget());
|
||||
refs.push(p.flyout.as_widget());
|
||||
}
|
||||
tree.diff_children(&refs);
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
Size::new(Length::Shrink, Length::Shrink)
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let max_w = limits.max().width;
|
||||
let natural =
|
||||
layout::Limits::new(Size::ZERO, Size::new(f32::INFINITY, f32::INFINITY));
|
||||
let n = self.panels.len();
|
||||
|
||||
// Measure every inline panel and every collapsed button.
|
||||
let mut inline_w = vec![0.0f32; n];
|
||||
let mut button_w = vec![0.0f32; n];
|
||||
for i in 0..n {
|
||||
let inline_node = self.panels[i].inline.as_widget_mut().layout(
|
||||
&mut tree.children[3 * i],
|
||||
renderer,
|
||||
&natural,
|
||||
);
|
||||
let button_node = self.panels[i].button.as_widget_mut().layout(
|
||||
&mut tree.children[3 * i + 1],
|
||||
renderer,
|
||||
&natural,
|
||||
);
|
||||
inline_w[i] = inline_node.size().width;
|
||||
button_w[i] = button_node.size().width;
|
||||
}
|
||||
|
||||
// Expand a prefix of panels from the left while the row still fits with
|
||||
// the remaining panels collapsed to buttons.
|
||||
let all_buttons: f32 = button_w.iter().sum();
|
||||
let mut used = all_buttons;
|
||||
let mut k = 0;
|
||||
for i in 0..n {
|
||||
let candidate = used - button_w[i] + inline_w[i];
|
||||
if candidate <= max_w {
|
||||
used = candidate;
|
||||
k = i + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.split.set(k);
|
||||
|
||||
// Place the chosen element for each panel left-to-right.
|
||||
let mut children: Vec<layout::Node> = Vec::with_capacity(n);
|
||||
let mut x = 0.0f32;
|
||||
for i in 0..n {
|
||||
let tree_idx = Self::shown_tree(i, k);
|
||||
let node = self.shown_mut(i, k).as_widget_mut().layout(
|
||||
&mut tree.children[tree_idx],
|
||||
renderer,
|
||||
&natural,
|
||||
);
|
||||
let h = node.size().height;
|
||||
let y = ((self.row_h - h) / 2.0).max(0.0);
|
||||
let w = node.size().width;
|
||||
children.push(node.move_to(Point::new(x, y)));
|
||||
x += w;
|
||||
}
|
||||
|
||||
layout::Node::with_children(Size::new(x, self.row_h), children)
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let k = self.split.get();
|
||||
for (i, child_layout) in layout.children().enumerate() {
|
||||
let tree_idx = Self::shown_tree(i, k);
|
||||
self.shown_mut(i, k).as_widget_mut().update(
|
||||
&mut tree.children[tree_idx],
|
||||
event,
|
||||
child_layout,
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
let k = self.split.get();
|
||||
let mut interaction = mouse::Interaction::default();
|
||||
for (i, child_layout) in layout.children().enumerate() {
|
||||
let tree_idx = Self::shown_tree(i, k);
|
||||
let it = self.shown(i, k).as_widget().mouse_interaction(
|
||||
&tree.children[tree_idx],
|
||||
child_layout,
|
||||
cursor,
|
||||
viewport,
|
||||
renderer,
|
||||
);
|
||||
if it != mouse::Interaction::default() {
|
||||
interaction = it;
|
||||
}
|
||||
}
|
||||
interaction
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
let k = self.split.get();
|
||||
for (i, child_layout) in layout.children().enumerate() {
|
||||
let tree_idx = Self::shown_tree(i, k);
|
||||
self.shown_mut(i, k).as_widget_mut().operate(
|
||||
&mut tree.children[tree_idx],
|
||||
child_layout,
|
||||
renderer,
|
||||
operation,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let k = self.split.get();
|
||||
for (i, child_layout) in layout.children().enumerate() {
|
||||
let tree_idx = Self::shown_tree(i, k);
|
||||
self.shown(i, k).as_widget().draw(
|
||||
&tree.children[tree_idx],
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
child_layout,
|
||||
cursor,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut widget::Tree,
|
||||
layout: Layout<'b>,
|
||||
_renderer: &Renderer,
|
||||
_viewport: &Rectangle,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||
let k = self.split.get();
|
||||
let open_id = self.open.clone()?;
|
||||
let p = self.panels.iter().position(|pan| pan.id == open_id)?;
|
||||
// Only collapsed panels (index >= k) show a flyout.
|
||||
if p < k {
|
||||
return None;
|
||||
}
|
||||
|
||||
let child_layout = layout.children().nth(p)?;
|
||||
let b = child_layout.bounds();
|
||||
let anchor = Point::new(b.x + translation.x, b.y + b.height + translation.y);
|
||||
|
||||
Some(overlay::Element::new(Box::new(FlyoutOverlay {
|
||||
flyout: &mut self.panels[p].flyout,
|
||||
tree: &mut tree.children[3 * p + 2],
|
||||
anchor,
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<CollapsePanels<'a>> for Element<'a, Message> {
|
||||
fn from(w: CollapsePanels<'a>) -> Self {
|
||||
Element::new(w)
|
||||
}
|
||||
}
|
||||
|
||||
/// Overlay that renders an open panel's flyout anchored below its button and
|
||||
/// closes it when the user presses outside.
|
||||
struct FlyoutOverlay<'a, 'b> {
|
||||
flyout: &'b mut Element<'a, Message>,
|
||||
tree: &'b mut widget::Tree,
|
||||
anchor: Point,
|
||||
}
|
||||
|
||||
impl overlay::Overlay<Message, Theme, Renderer> for FlyoutOverlay<'_, '_> {
|
||||
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
|
||||
let viewport = Rectangle::with_size(bounds);
|
||||
let limits = layout::Limits::new(Size::ZERO, viewport.size());
|
||||
let node = self
|
||||
.flyout
|
||||
.as_widget_mut()
|
||||
.layout(self.tree, renderer, &limits);
|
||||
let size = node.size();
|
||||
let mut x = self.anchor.x;
|
||||
let mut y = self.anchor.y;
|
||||
if x + size.width > viewport.width {
|
||||
x = (viewport.width - size.width).max(0.0);
|
||||
}
|
||||
if y + size.height > viewport.height {
|
||||
y = (self.anchor.y - size.height).max(0.0);
|
||||
}
|
||||
layout::Node::with_children(size, vec![node]).translate(Vector::new(x, y))
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
) {
|
||||
let child = layout.children().next().unwrap();
|
||||
self.flyout.as_widget().draw(
|
||||
self.tree,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
child,
|
||||
cursor,
|
||||
&child.bounds(),
|
||||
);
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) {
|
||||
let child = layout.children().next().unwrap();
|
||||
let vp = child.bounds();
|
||||
|
||||
// Close when a press lands outside the flyout. Capture the event so the
|
||||
// press doesn't also reach the collapsed button underneath (which would
|
||||
// immediately re-open the flyout).
|
||||
if let Event::Mouse(mouse::Event::ButtonPressed(_)) = event {
|
||||
if !cursor.is_over(vp) {
|
||||
shell.publish(Message::CloseRibbonDropdown);
|
||||
shell.capture_event();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.flyout
|
||||
.as_widget_mut()
|
||||
.update(self.tree, event, child, cursor, renderer, clipboard, shell, &vp);
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&mut self,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
let child = layout.children().next().unwrap();
|
||||
self.flyout
|
||||
.as_widget_mut()
|
||||
.operate(self.tree, child, renderer, operation);
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
let child = layout.children().next().unwrap();
|
||||
self.flyout
|
||||
.as_widget()
|
||||
.mouse_interaction(self.tree, child, cursor, &child.bounds(), renderer)
|
||||
}
|
||||
}
|
||||
+332
-82
@@ -22,6 +22,8 @@ use crate::ui::properties::{lw_options, LinetypeItem};
|
||||
|
||||
mod widgets;
|
||||
use widgets::{StyleContext, *};
|
||||
mod collapse;
|
||||
use collapse::{CollapsePanels, Panel};
|
||||
use crate::ui::wrap_bar::{DensitySwap, WrapBar, WrapFlow};
|
||||
|
||||
// ── Ribbon state ───────────────────────────────────────────────────────────
|
||||
@@ -33,6 +35,11 @@ pub struct Ribbon {
|
||||
pub wireframe: bool,
|
||||
pub ortho_mode: bool,
|
||||
pub open_dropdown: Option<String>,
|
||||
/// Title of the collapsed panel whose flyout is currently open, if any.
|
||||
pub collapsed_open: Option<String>,
|
||||
/// Last tool used from each panel (panel title → tool id). A collapsed panel
|
||||
/// shows this tool on its button, defaulting to the panel's first tool.
|
||||
last_panel_tool: HashMap<&'static str, &'static str>,
|
||||
last_cmd: HashMap<&'static str, &'static str>,
|
||||
pub layer_names: Vec<String>,
|
||||
pub active_layer: String,
|
||||
@@ -93,6 +100,8 @@ impl Ribbon {
|
||||
wireframe: false,
|
||||
ortho_mode: true,
|
||||
open_dropdown: None,
|
||||
collapsed_open: None,
|
||||
last_panel_tool: HashMap::default(),
|
||||
last_cmd: HashMap::default(),
|
||||
// Empty until a document is open — populated by sync_ribbon_layers.
|
||||
layer_names: vec![],
|
||||
@@ -131,6 +140,17 @@ impl Ribbon {
|
||||
f32::from_bits(self.tool_bar_h.load(Ordering::Relaxed))
|
||||
}
|
||||
|
||||
/// (top, left) screen offset at which to anchor the open dropdown `id`'s
|
||||
/// overlay — just below its widget. Falls back to below the whole ribbon
|
||||
/// when the widget's position hasn't been recorded yet.
|
||||
fn dd_anchor(&self, id: &str) -> (f32, f32) {
|
||||
if let Some(b) = crate::ui::wrap_bar::dropdown_bounds(id) {
|
||||
(b.y + b.height, b.x)
|
||||
} else {
|
||||
(self.tab_bar_height() + self.tool_bar_height(), 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_styles(
|
||||
&mut self,
|
||||
text: Vec<String>,
|
||||
@@ -209,6 +229,34 @@ impl Ribbon {
|
||||
}
|
||||
pub fn close_dropdown(&mut self) {
|
||||
self.open_dropdown = None;
|
||||
self.collapsed_open = None;
|
||||
}
|
||||
|
||||
/// Toggle the flyout of a collapsed ribbon panel (identified by its title).
|
||||
pub fn toggle_collapsed_panel(&mut self, id: &str) {
|
||||
if self.collapsed_open.as_deref() == Some(id) {
|
||||
self.collapsed_open = None;
|
||||
} else {
|
||||
self.collapsed_open = Some(id.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/// Record `tool_id` as the last-used tool of whichever active-module panel
|
||||
/// contains it, so a collapsed panel shows that tool on its button.
|
||||
pub fn note_panel_tool(&mut self, tool_id: &str) {
|
||||
if let Some(module) = self.modules.get(self.active) {
|
||||
for group in module.ribbon_groups() {
|
||||
if let Some(id) = group
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(item_id)
|
||||
.find(|id| *id == tool_id)
|
||||
{
|
||||
self.last_panel_tool.insert(group.title, id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the index of the Layout module in the modules list.
|
||||
@@ -386,9 +434,10 @@ impl Ribbon {
|
||||
};
|
||||
|
||||
// Adaptive tool area, widest→narrowest:
|
||||
// 1. full — full-size panels on one row
|
||||
// 2. compact — large buttons shrink to icon-only columns
|
||||
// 3. wrapped — compact panels flex-wrap onto extra rows
|
||||
// 1. full — full-size panels on one row
|
||||
// 2. compact — large buttons shrink to icon-only columns
|
||||
// 3. collapse — panels that still don't fit collapse to title
|
||||
// buttons that open the full panel as a flyout
|
||||
// DensitySwap shows the widest variant that fits the width.
|
||||
let build = |compact: bool| {
|
||||
build_tool_groups(
|
||||
@@ -419,9 +468,72 @@ impl Ribbon {
|
||||
r.push(w)
|
||||
})
|
||||
.into();
|
||||
let wrapped: Element<'_, Message> =
|
||||
WrapFlow::new(build(true)).spacing_x(0.0).row_h(TOOL_BAR_H).into();
|
||||
DensitySwap::new(vec![full, compact, wrapped])
|
||||
|
||||
let panels: Vec<Panel<'_>> = groups
|
||||
.iter()
|
||||
.map(|g| Panel {
|
||||
id: g.title.to_string(),
|
||||
inline: render_group(
|
||||
true,
|
||||
g,
|
||||
&self.active_tool,
|
||||
&self.open_dropdown,
|
||||
&self.last_cmd,
|
||||
self.wireframe,
|
||||
self.ortho_mode,
|
||||
&self.layer_infos,
|
||||
&self.active_layer,
|
||||
self.active_color,
|
||||
&self.active_linetype,
|
||||
self.active_lineweight,
|
||||
&style_ctx,
|
||||
),
|
||||
button: collapse_button(
|
||||
g,
|
||||
self.last_panel_tool.get(g.title).copied(),
|
||||
&self.active_tool,
|
||||
&self.open_dropdown,
|
||||
&self.last_cmd,
|
||||
self.wireframe,
|
||||
self.ortho_mode,
|
||||
&self.layer_infos,
|
||||
&self.active_layer,
|
||||
self.active_color,
|
||||
&self.active_linetype,
|
||||
self.active_lineweight,
|
||||
&style_ctx,
|
||||
),
|
||||
flyout: container(render_group(
|
||||
false,
|
||||
g,
|
||||
&self.active_tool,
|
||||
&self.open_dropdown,
|
||||
&self.last_cmd,
|
||||
self.wireframe,
|
||||
self.ortho_mode,
|
||||
&self.layer_infos,
|
||||
&self.active_layer,
|
||||
self.active_color,
|
||||
&self.active_linetype,
|
||||
self.active_lineweight,
|
||||
&style_ctx,
|
||||
))
|
||||
.style(|_: &Theme| container::Style {
|
||||
background: Some(Background::Color(RIBBON_BG)),
|
||||
border: Border {
|
||||
color: BORDER_DARK,
|
||||
width: 1.0,
|
||||
radius: 0.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.into(),
|
||||
})
|
||||
.collect();
|
||||
let collapse: Element<'_, Message> =
|
||||
CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H).into();
|
||||
|
||||
DensitySwap::new(vec![full, compact, collapse])
|
||||
.report_height(self.tool_bar_h.clone())
|
||||
.into()
|
||||
} else {
|
||||
@@ -495,12 +607,13 @@ impl Ribbon {
|
||||
})
|
||||
.width(Length::Fixed(170.0));
|
||||
|
||||
let (top, left) = self.dd_anchor(open_id);
|
||||
let positioned = container(panel)
|
||||
.align_left(Fill)
|
||||
.align_top(Fill)
|
||||
.padding(Padding {
|
||||
top: 28.0,
|
||||
left: compute_history_dropdown_left(open_id),
|
||||
top,
|
||||
left,
|
||||
..Default::default()
|
||||
})
|
||||
.width(Fill)
|
||||
@@ -619,8 +732,7 @@ impl Ribbon {
|
||||
})
|
||||
.width(Length::Fixed(190.0));
|
||||
|
||||
let top_offset = self.tab_bar_height() + self.tool_bar_height();
|
||||
let left_offset = compute_dropdown_left(&groups, open_id);
|
||||
let (top_offset, left_offset) = self.dd_anchor(open_id);
|
||||
let positioned = container(panel)
|
||||
.align_left(Fill)
|
||||
.align_top(Fill)
|
||||
@@ -1113,85 +1225,223 @@ fn build_tool_groups<'a>(
|
||||
|
||||
for group in groups {
|
||||
if !first_group {
|
||||
widgets.push(
|
||||
container(text(""))
|
||||
.width(1)
|
||||
.height(Length::Fixed(TOOL_BAR_H))
|
||||
.style(|_: &Theme| container::Style {
|
||||
background: Some(Background::Color(BORDER_DARK)),
|
||||
..Default::default()
|
||||
})
|
||||
.into(),
|
||||
);
|
||||
widgets.push(tool_divider());
|
||||
}
|
||||
first_group = false;
|
||||
|
||||
let mut items_row: Vec<Element<Message>> = Vec::new();
|
||||
let mut small_buf: Vec<Element<Message>> = Vec::new();
|
||||
|
||||
for item in &group.tools {
|
||||
let is_large = match item {
|
||||
RibbonItem::LargeTool(_) | RibbonItem::LargeDropdown { .. } => !compact,
|
||||
RibbonItem::LayerComboGroup { .. }
|
||||
| RibbonItem::PropertiesGroup { .. }
|
||||
| RibbonItem::StyleComboGroup { .. } => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if is_large {
|
||||
flush_small_col(&mut small_buf, &mut items_row);
|
||||
items_row.push(render_large(
|
||||
item,
|
||||
active_tool,
|
||||
open_dd,
|
||||
last_cmd,
|
||||
wireframe,
|
||||
ortho_mode,
|
||||
layer_infos,
|
||||
active_layer,
|
||||
active_color,
|
||||
active_linetype,
|
||||
active_lineweight,
|
||||
style_ctx,
|
||||
));
|
||||
} else {
|
||||
small_buf.push(render_small(
|
||||
item,
|
||||
active_tool,
|
||||
open_dd,
|
||||
last_cmd,
|
||||
wireframe,
|
||||
ortho_mode,
|
||||
));
|
||||
if small_buf.len() == 3 {
|
||||
flush_small_col(&mut small_buf, &mut items_row);
|
||||
}
|
||||
}
|
||||
}
|
||||
flush_small_col(&mut small_buf, &mut items_row);
|
||||
|
||||
let tools_el = items_row
|
||||
.into_iter()
|
||||
.fold(row![].spacing(2).height(Fill).align_y(iced::Top), |r, e| {
|
||||
r.push(e)
|
||||
});
|
||||
|
||||
widgets.push(
|
||||
column![
|
||||
tools_el,
|
||||
container(text(group.title).size(9).color(GROUP_LABEL)).padding([1, 4]),
|
||||
]
|
||||
.align_x(iced::Center)
|
||||
.spacing(0)
|
||||
.padding([3u16, 4])
|
||||
.height(Length::Fixed(TOOL_BAR_H))
|
||||
.into(),
|
||||
);
|
||||
widgets.push(render_group(
|
||||
compact,
|
||||
group,
|
||||
active_tool,
|
||||
open_dd,
|
||||
last_cmd,
|
||||
wireframe,
|
||||
ortho_mode,
|
||||
layer_infos,
|
||||
active_layer,
|
||||
active_color,
|
||||
active_linetype,
|
||||
active_lineweight,
|
||||
style_ctx,
|
||||
));
|
||||
}
|
||||
|
||||
widgets
|
||||
}
|
||||
|
||||
/// A 1px vertical divider between ribbon panels, full tool-area height.
|
||||
fn tool_divider<'a>() -> Element<'a, Message> {
|
||||
container(text(""))
|
||||
.width(1)
|
||||
.height(Length::Fixed(TOOL_BAR_H))
|
||||
.style(|_: &Theme| container::Style {
|
||||
background: Some(Background::Color(BORDER_DARK)),
|
||||
..Default::default()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Render a single ribbon panel (tools + group label), fixed `TOOL_BAR_H` tall.
|
||||
/// When `compact`, large tools/dropdowns are drawn as small icon columns.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_group<'a>(
|
||||
compact: bool,
|
||||
group: &RibbonGroup,
|
||||
active_tool: &Option<String>,
|
||||
open_dd: &Option<String>,
|
||||
last_cmd: &HashMap<&'static str, &'static str>,
|
||||
wireframe: bool,
|
||||
ortho_mode: bool,
|
||||
layer_infos: &'a [LayerInfo],
|
||||
active_layer: &'a str,
|
||||
active_color: AcadColor,
|
||||
active_linetype: &'a str,
|
||||
active_lineweight: LineWeight,
|
||||
style_ctx: &StyleContext,
|
||||
) -> Element<'a, Message> {
|
||||
let mut items_row: Vec<Element<Message>> = Vec::new();
|
||||
let mut small_buf: Vec<Element<Message>> = Vec::new();
|
||||
|
||||
for item in &group.tools {
|
||||
let is_large = match item {
|
||||
RibbonItem::LargeTool(_) | RibbonItem::LargeDropdown { .. } => !compact,
|
||||
RibbonItem::LayerComboGroup { .. }
|
||||
| RibbonItem::PropertiesGroup { .. }
|
||||
| RibbonItem::StyleComboGroup { .. } => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if is_large {
|
||||
flush_small_col(&mut small_buf, &mut items_row);
|
||||
items_row.push(render_large(
|
||||
item,
|
||||
active_tool,
|
||||
open_dd,
|
||||
last_cmd,
|
||||
wireframe,
|
||||
ortho_mode,
|
||||
layer_infos,
|
||||
active_layer,
|
||||
active_color,
|
||||
active_linetype,
|
||||
active_lineweight,
|
||||
style_ctx,
|
||||
));
|
||||
} else {
|
||||
small_buf.push(render_small(
|
||||
item,
|
||||
active_tool,
|
||||
open_dd,
|
||||
last_cmd,
|
||||
wireframe,
|
||||
ortho_mode,
|
||||
));
|
||||
if small_buf.len() == 3 {
|
||||
flush_small_col(&mut small_buf, &mut items_row);
|
||||
}
|
||||
}
|
||||
}
|
||||
flush_small_col(&mut small_buf, &mut items_row);
|
||||
|
||||
let tools_el = items_row
|
||||
.into_iter()
|
||||
.fold(row![].spacing(2).height(Fill).align_y(iced::Top), |r, e| {
|
||||
r.push(e)
|
||||
});
|
||||
|
||||
column![
|
||||
tools_el,
|
||||
container(text(group.title).size(9).color(GROUP_LABEL)).padding([1, 4]),
|
||||
]
|
||||
.align_x(iced::Center)
|
||||
.spacing(0)
|
||||
.padding([3u16, 4])
|
||||
.height(Length::Fixed(TOOL_BAR_H))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// The top-level command id of a ribbon item, if it has one.
|
||||
fn item_id(it: &RibbonItem) -> Option<&'static str> {
|
||||
match it {
|
||||
RibbonItem::Tool(t) | RibbonItem::LargeTool(t) => Some(t.id),
|
||||
RibbonItem::Dropdown { id, .. } | RibbonItem::LargeDropdown { id, .. } => Some(*id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The tool a collapsed panel shows on its button: the last-used one, else the
|
||||
/// panel's first tool-like item.
|
||||
fn representative<'g>(group: &'g RibbonGroup, last_used: Option<&str>) -> Option<&'g RibbonItem> {
|
||||
if let Some(want) = last_used {
|
||||
if let Some(found) = group
|
||||
.tools
|
||||
.iter()
|
||||
.find(|&it| item_id(it).map_or(false, |id| id == want))
|
||||
{
|
||||
return Some(found);
|
||||
}
|
||||
}
|
||||
group.tools.iter().find(|&it| item_id(it).is_some())
|
||||
}
|
||||
|
||||
/// A collapsed panel: its representative tool (a live button that updates to the
|
||||
/// last-used tool) plus a title + ▾ opener for the full flyout. Fixed
|
||||
/// `TOOL_BAR_H` tall so it lines up with inline panels.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn collapse_button<'a>(
|
||||
group: &RibbonGroup,
|
||||
last_used: Option<&str>,
|
||||
active_tool: &Option<String>,
|
||||
open_dd: &Option<String>,
|
||||
last_cmd: &HashMap<&'static str, &'static str>,
|
||||
wireframe: bool,
|
||||
ortho_mode: bool,
|
||||
layer_infos: &'a [LayerInfo],
|
||||
active_layer: &'a str,
|
||||
active_color: AcadColor,
|
||||
active_linetype: &'a str,
|
||||
active_lineweight: LineWeight,
|
||||
style_ctx: &StyleContext,
|
||||
) -> Element<'a, Message> {
|
||||
let title = group.title;
|
||||
|
||||
// Show the representative tool with a large icon (Tool/Dropdown alike —
|
||||
// render_large handles every tool-like item).
|
||||
let face: Element<'_, Message> = match representative(group, last_used) {
|
||||
Some(item) => render_large(
|
||||
item,
|
||||
active_tool,
|
||||
open_dd,
|
||||
last_cmd,
|
||||
wireframe,
|
||||
ortho_mode,
|
||||
layer_infos,
|
||||
active_layer,
|
||||
active_color,
|
||||
active_linetype,
|
||||
active_lineweight,
|
||||
style_ctx,
|
||||
),
|
||||
None => text("").into(),
|
||||
};
|
||||
|
||||
let opener = button(
|
||||
row![
|
||||
text(title.to_string()).size(9).color(GROUP_LABEL),
|
||||
crate::ui::icons::arrow_down(8.0, GROUP_LABEL),
|
||||
]
|
||||
.spacing(3)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.on_press(Message::ToggleRibbonPanel(title.to_string()))
|
||||
.style(|_: &Theme, status| button::Style {
|
||||
background: Some(Background::Color(match status {
|
||||
button::Status::Hovered => Color {
|
||||
r: 0.25,
|
||||
g: 0.25,
|
||||
b: 0.25,
|
||||
a: 1.0,
|
||||
},
|
||||
_ => Color::TRANSPARENT,
|
||||
})),
|
||||
border: Border {
|
||||
radius: 2.0.into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.padding([1, 4]);
|
||||
|
||||
column![
|
||||
container(face).height(Fill).align_y(iced::Center),
|
||||
opener,
|
||||
]
|
||||
.align_x(iced::Center)
|
||||
.spacing(2)
|
||||
.padding([3u16, 4])
|
||||
.height(Length::Fixed(TOOL_BAR_H))
|
||||
.into()
|
||||
}
|
||||
|
||||
impl Default for Ribbon {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
|
||||
+132
-111
@@ -13,6 +13,7 @@ use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::modules::{IconKind, ModuleEvent, RibbonGroup, RibbonItem, StyleKey, ToolDef};
|
||||
use crate::ui::wrap_bar::PosReport;
|
||||
use crate::ui::icons;
|
||||
use crate::ui::properties::{acad_color_display, LwItem};
|
||||
|
||||
@@ -453,10 +454,11 @@ pub(super) fn render_small<'a>(
|
||||
.delay(Duration::from_millis(400))
|
||||
.style(tip_style);
|
||||
|
||||
row![icon_with_tip, arr_with_tip]
|
||||
.spacing(0)
|
||||
.height(ROW_H)
|
||||
.into()
|
||||
PosReport::new(
|
||||
*id,
|
||||
row![icon_with_tip, arr_with_tip].spacing(0).height(ROW_H),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
_ => text("").into(),
|
||||
@@ -465,6 +467,109 @@ pub(super) fn render_small<'a>(
|
||||
|
||||
// ── Large item renderer ────────────────────────────────────────────────────
|
||||
|
||||
/// A large dropdown button: the current icon on top, its ▾ directly beneath the
|
||||
/// icon, then the label at the bottom. Shared by `LargeDropdown` / `Dropdown` in
|
||||
/// the full ribbon and by a collapsed panel whose representative is a dropdown.
|
||||
/// `explicit_label` overrides the derived (current-item) label when given.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn render_large_dropdown<'a>(
|
||||
id: &'static str,
|
||||
icon: IconKind,
|
||||
explicit_label: Option<&str>,
|
||||
items: &[(&'static str, &'static str, IconKind)],
|
||||
default: &'static str,
|
||||
active_tool: &Option<String>,
|
||||
open_dd: &Option<String>,
|
||||
last_cmd: &HashMap<&'static str, &'static str>,
|
||||
) -> Element<'a, Message> {
|
||||
let active = active_tool.as_deref() == Some(id)
|
||||
|| items
|
||||
.iter()
|
||||
.any(|(cmd, _, _)| active_tool.as_deref() == Some(*cmd));
|
||||
let dd_open = open_dd.as_deref() == Some(id);
|
||||
let last = last_cmd.get(id).copied().unwrap_or(default);
|
||||
let cur_icon = last_cmd
|
||||
.get(id)
|
||||
.copied()
|
||||
.and_then(|cmd| items.iter().find(|(c, _, _)| *c == cmd).map(|(_, _, ik)| *ik))
|
||||
.or_else(|| items.first().map(|(_, _, ik)| *ik))
|
||||
.unwrap_or(icon);
|
||||
let cur_label = last_cmd
|
||||
.get(id)
|
||||
.copied()
|
||||
.and_then(|cmd| items.iter().find(|(c, _, _)| *c == cmd).map(|(_, lbl, _)| *lbl))
|
||||
.or_else(|| items.first().map(|(_, lbl, _)| *lbl))
|
||||
.unwrap_or(id);
|
||||
let label = explicit_label.unwrap_or(cur_label);
|
||||
let tip_text = format!("{}\nCommand: {}", cur_label, last);
|
||||
let arr_tip = format!("{} options", label);
|
||||
|
||||
// Icon on top with the label beneath it, then the ▾ strip at the very bottom.
|
||||
let top_btn = button(
|
||||
column![
|
||||
make_icon(cur_icon, LARGE_ICON),
|
||||
text(label.to_string()).size(10).color(LABEL_COLOR),
|
||||
]
|
||||
.align_x(iced::Center)
|
||||
.spacing(3),
|
||||
)
|
||||
.on_press(Message::RibbonToolClick {
|
||||
tool_id: last.to_string(),
|
||||
event: ModuleEvent::Command(last.to_string()),
|
||||
})
|
||||
.style(move |_: &Theme, status| tool_btn_style(active, status))
|
||||
.width(Length::Fixed(LARGE_W))
|
||||
.height(Fill)
|
||||
.padding(Padding {
|
||||
top: 6.0,
|
||||
right: 4.0,
|
||||
bottom: 2.0,
|
||||
left: 4.0,
|
||||
});
|
||||
|
||||
let arr_btn = button(
|
||||
container(icons::arrow_down(9.0, ARROW_COLOR))
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.align_x(iced::Center)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.on_press(Message::ToggleRibbonDropdown(id.to_string()))
|
||||
.style(move |_: &Theme, status| button::Style {
|
||||
background: Some(Background::Color(match status {
|
||||
button::Status::Hovered | button::Status::Pressed => TOOL_HOVER,
|
||||
_ if dd_open => TOOL_ACTIVE,
|
||||
_ => Color::TRANSPARENT,
|
||||
})),
|
||||
border: Border {
|
||||
radius: 3.0.into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.width(Length::Fixed(LARGE_W))
|
||||
.height(LARGE_ARR)
|
||||
.padding(0);
|
||||
|
||||
let top_with_tip = tooltip(top_btn, make_tip(tip_text), TipPos::Right)
|
||||
.gap(6.0)
|
||||
.delay(Duration::from_millis(400))
|
||||
.style(tip_style);
|
||||
let arr_with_tip = tooltip(arr_btn, make_tip(arr_tip), TipPos::Right)
|
||||
.gap(6.0)
|
||||
.delay(Duration::from_millis(400))
|
||||
.style(tip_style);
|
||||
|
||||
PosReport::new(
|
||||
id,
|
||||
column![top_with_tip, arr_with_tip]
|
||||
.spacing(0)
|
||||
.width(Length::Fixed(LARGE_W))
|
||||
.height(Fill),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Render a full-height large button (LargeTool, LargeDropdown, LayerCombo, StyleCombo).
|
||||
pub(super) fn render_large<'a>(
|
||||
item: &RibbonItem,
|
||||
@@ -481,7 +586,9 @@ pub(super) fn render_large<'a>(
|
||||
style_ctx: &StyleContext,
|
||||
) -> Element<'a, Message> {
|
||||
match item {
|
||||
RibbonItem::LargeTool(t) => {
|
||||
// A plain Tool renders large too, so a collapsed panel can show its
|
||||
// representative tool as a big icon.
|
||||
RibbonItem::LargeTool(t) | RibbonItem::Tool(t) => {
|
||||
let active = is_active_tool(t.id, active_tool, wireframe, ortho_mode);
|
||||
let event = t.event.clone();
|
||||
let tool_id = t.id.to_string();
|
||||
@@ -517,100 +624,27 @@ pub(super) fn render_large<'a>(
|
||||
icon,
|
||||
items,
|
||||
default,
|
||||
} => {
|
||||
let active = active_tool.as_deref() == Some(*id)
|
||||
|| items
|
||||
.iter()
|
||||
.any(|(cmd, _, _)| active_tool.as_deref() == Some(*cmd));
|
||||
let dd_open = open_dd.as_deref() == Some(*id);
|
||||
let last = last_cmd.get(id).copied().unwrap_or(*default);
|
||||
let cur_icon = last_cmd
|
||||
.get(id)
|
||||
.copied()
|
||||
.and_then(|cmd| {
|
||||
items
|
||||
.iter()
|
||||
.find(|(c, _, _)| *c == cmd)
|
||||
.map(|(_, _, ik)| *ik)
|
||||
})
|
||||
.or_else(|| items.first().map(|(_, _, ik)| *ik))
|
||||
.unwrap_or(*icon);
|
||||
} => render_large_dropdown(
|
||||
*id,
|
||||
*icon,
|
||||
Some(*label),
|
||||
items,
|
||||
*default,
|
||||
active_tool,
|
||||
open_dd,
|
||||
last_cmd,
|
||||
),
|
||||
|
||||
let cur_label = last_cmd
|
||||
.get(id)
|
||||
.copied()
|
||||
.and_then(|cmd| {
|
||||
items
|
||||
.iter()
|
||||
.find(|(c, _, _)| *c == cmd)
|
||||
.map(|(_, lbl, _)| *lbl)
|
||||
})
|
||||
.or_else(|| items.first().map(|(_, lbl, _)| *lbl))
|
||||
.unwrap_or(*label);
|
||||
let tip_text = format!("{}\nCommand: {}", cur_label, last);
|
||||
let arr_tip = format!("{} options", label);
|
||||
|
||||
let top_btn = button(
|
||||
column![
|
||||
make_icon(cur_icon, LARGE_ICON),
|
||||
text(*label).size(10).color(LABEL_COLOR),
|
||||
]
|
||||
.align_x(iced::Center)
|
||||
.spacing(3),
|
||||
)
|
||||
.on_press(Message::RibbonToolClick {
|
||||
tool_id: last.to_string(),
|
||||
event: ModuleEvent::Command(last.to_string()),
|
||||
})
|
||||
.style(move |_: &Theme, status| tool_btn_style(active, status))
|
||||
.width(Length::Fixed(LARGE_W))
|
||||
.height(Fill)
|
||||
.padding(Padding {
|
||||
top: 6.0,
|
||||
right: 4.0,
|
||||
bottom: 2.0,
|
||||
left: 4.0,
|
||||
});
|
||||
|
||||
let arr_btn = button(
|
||||
container(icons::arrow_down(9.0, ARROW_COLOR))
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.align_x(iced::Center)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.on_press(Message::ToggleRibbonDropdown(id.to_string()))
|
||||
.style(move |_: &Theme, status| button::Style {
|
||||
background: Some(Background::Color(match status {
|
||||
button::Status::Hovered | button::Status::Pressed => TOOL_HOVER,
|
||||
_ if dd_open => TOOL_ACTIVE,
|
||||
_ => Color::TRANSPARENT,
|
||||
})),
|
||||
border: Border {
|
||||
radius: 3.0.into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.width(Length::Fixed(LARGE_W))
|
||||
.height(LARGE_ARR)
|
||||
.padding(0);
|
||||
|
||||
let top_with_tip = tooltip(top_btn, make_tip(tip_text), TipPos::Right)
|
||||
.gap(6.0)
|
||||
.delay(Duration::from_millis(400))
|
||||
.style(tip_style);
|
||||
let arr_with_tip = tooltip(arr_btn, make_tip(arr_tip), TipPos::Right)
|
||||
.gap(6.0)
|
||||
.delay(Duration::from_millis(400))
|
||||
.style(tip_style);
|
||||
|
||||
column![top_with_tip, arr_with_tip]
|
||||
.spacing(0)
|
||||
.width(Length::Fixed(LARGE_W))
|
||||
.height(Fill)
|
||||
.into()
|
||||
}
|
||||
// A plain Dropdown renders large too (used by a collapsed panel whose
|
||||
// representative tool is a dropdown).
|
||||
RibbonItem::Dropdown {
|
||||
id,
|
||||
icon,
|
||||
items,
|
||||
default,
|
||||
} => render_large_dropdown(
|
||||
*id, *icon, None, items, *default, active_tool, open_dd, last_cmd,
|
||||
),
|
||||
|
||||
RibbonItem::LayerComboGroup { row2, row3 } => {
|
||||
const COMBO_W: f32 = LARGE_W * 2.5;
|
||||
@@ -953,8 +987,6 @@ pub(super) fn render_large<'a>(
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
_ => text("").into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1059,17 +1091,6 @@ pub(super) fn compute_prop_combo_left(groups: &[RibbonGroup], _dd_id: &str) -> f
|
||||
base + LARGE_W + 4.0
|
||||
}
|
||||
|
||||
pub(super) fn compute_history_dropdown_left(open_id: &str) -> f32 {
|
||||
let logo_w = 38.0;
|
||||
let leading_gap = 6.0;
|
||||
let ctrl_w = TOP_HIST_W + TOP_ARR_W;
|
||||
|
||||
match open_id {
|
||||
UNDO_HISTORY_ID => logo_w + leading_gap,
|
||||
REDO_HISTORY_ID => logo_w + leading_gap + ctrl_w + TOP_HIST_GAP,
|
||||
_ => logo_w + leading_gap,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Message helpers ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
+156
-1
@@ -14,10 +14,12 @@
|
||||
//! The measured total height is written to `height_out` (if set) so callers can
|
||||
//! anchor overlays below the possibly-taller bar.
|
||||
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::cell::Cell;
|
||||
use std::sync::Arc;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use iced::advanced::layout::{self, Layout};
|
||||
use iced::advanced::widget::{self, Widget};
|
||||
use iced::advanced::{mouse, overlay, renderer, Clipboard, Shell};
|
||||
@@ -25,6 +27,19 @@ use iced::{Element, Event, Length, Point, Rectangle, Renderer, Size, Theme, Vect
|
||||
|
||||
use crate::app::Message;
|
||||
|
||||
thread_local! {
|
||||
/// Screen bounds of every ribbon dropdown button, keyed by dropdown id, so
|
||||
/// an open dropdown's overlay can anchor directly below its widget at any
|
||||
/// size. Written by `PosReport` on draw, read by `dropdown_bounds`.
|
||||
static DD_BOUNDS: RefCell<FxHashMap<&'static str, Rectangle>> =
|
||||
RefCell::new(FxHashMap::default());
|
||||
}
|
||||
|
||||
/// Last-drawn screen bounds of the dropdown button with this id.
|
||||
pub fn dropdown_bounds(id: &str) -> Option<Rectangle> {
|
||||
DD_BOUNDS.with(|m| m.borrow().get(id).copied())
|
||||
}
|
||||
|
||||
pub struct WrapBar<'a> {
|
||||
lead: Element<'a, Message>,
|
||||
/// Optional Fill filler occupying the gap between `lead` and `trail`.
|
||||
@@ -802,3 +817,143 @@ impl<'a> From<DensitySwap<'a>> for Element<'a, Message> {
|
||||
Element::new(w)
|
||||
}
|
||||
}
|
||||
|
||||
/// A transparent wrapper that records its child's screen bounds under `id` on
|
||||
/// every draw, so an open dropdown can anchor its overlay just below the widget.
|
||||
pub struct PosReport<'a> {
|
||||
id: &'static str,
|
||||
child: Element<'a, Message>,
|
||||
}
|
||||
|
||||
impl<'a> PosReport<'a> {
|
||||
pub fn new(id: &'static str, child: impl Into<Element<'a, Message>>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
child: child.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget<Message, Theme, Renderer> for PosReport<'a> {
|
||||
fn children(&self) -> Vec<widget::Tree> {
|
||||
vec![widget::Tree::new(&self.child)]
|
||||
}
|
||||
|
||||
fn diff(&self, tree: &mut widget::Tree) {
|
||||
tree.diff_children(&[self.child.as_widget()]);
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
self.child.as_widget().size()
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> Size<Length> {
|
||||
self.child.as_widget().size_hint()
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
self.child
|
||||
.as_widget_mut()
|
||||
.layout(&mut tree.children[0], renderer, limits)
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.child.as_widget_mut().update(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
layout,
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.child
|
||||
.as_widget()
|
||||
.mouse_interaction(&tree.children[0], layout, cursor, viewport, renderer)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
self.child
|
||||
.as_widget_mut()
|
||||
.operate(&mut tree.children[0], layout, renderer, operation);
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
DD_BOUNDS.with(|m| {
|
||||
m.borrow_mut().insert(self.id, layout.bounds());
|
||||
});
|
||||
self.child.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut widget::Tree,
|
||||
layout: Layout<'b>,
|
||||
renderer: &Renderer,
|
||||
viewport: &Rectangle,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||
self.child.as_widget_mut().overlay(
|
||||
&mut tree.children[0],
|
||||
layout,
|
||||
renderer,
|
||||
viewport,
|
||||
translation,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<PosReport<'a>> for Element<'a, Message> {
|
||||
fn from(w: PosReport<'a>) -> Self {
|
||||
Element::new(w)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user