feat(ribbon): shrink large tools to compact icons when the ribbon is narrow
Add a DensitySwap widget that renders the widest variant of the tool area that fits the current width. The ribbon builds two variants via the new build_tool_row(compact) helper: full-size panels, and a compact one where LargeTool/LargeDropdown collapse to small icon columns (the layer/properties/style combos always stay full). render_small now also matches the Large* variants so they can be drawn small. First stage of the adaptive tool area (shrink -> wrap -> collapse). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a065dc939f
commit
65dfd4e462
3 changed files with 330 additions and 99 deletions
|
|
@ -16,13 +16,13 @@ use iced::widget::{button, column, container, mouse_area, row, scrollable, svg,
|
||||||
use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
|
use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::modules::{CadModule, IconKind, RibbonItem};
|
use crate::modules::{CadModule, IconKind, RibbonGroup, RibbonItem};
|
||||||
use crate::plugin::all_ribbon_modules;
|
use crate::plugin::all_ribbon_modules;
|
||||||
use crate::ui::properties::{lw_options, LinetypeItem};
|
use crate::ui::properties::{lw_options, LinetypeItem};
|
||||||
|
|
||||||
mod widgets;
|
mod widgets;
|
||||||
use widgets::{StyleContext, *};
|
use widgets::{StyleContext, *};
|
||||||
use crate::ui::wrap_bar::{WrapBar, WrapFlow};
|
use crate::ui::wrap_bar::{DensitySwap, WrapBar, WrapFlow};
|
||||||
|
|
||||||
// ── Ribbon state ───────────────────────────────────────────────────────────
|
// ── Ribbon state ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -365,16 +365,6 @@ impl Ribbon {
|
||||||
let tool_area: Element<'_, Message> =
|
let tool_area: Element<'_, Message> =
|
||||||
if let Some(module) = self.modules.get(effective_active) {
|
if let Some(module) = self.modules.get(effective_active) {
|
||||||
let groups = module.ribbon_groups();
|
let groups = module.ribbon_groups();
|
||||||
let wireframe = self.wireframe;
|
|
||||||
let ortho_mode = self.ortho_mode;
|
|
||||||
let active_tool = self.active_tool.clone();
|
|
||||||
let open_dd = self.open_dropdown.clone();
|
|
||||||
let last_cmd = &self.last_cmd;
|
|
||||||
let layer_infos = &self.layer_infos;
|
|
||||||
let active_layer = &self.active_layer;
|
|
||||||
let active_color = self.active_color;
|
|
||||||
let active_linetype = &self.active_linetype;
|
|
||||||
let active_lineweight = self.active_lineweight;
|
|
||||||
let style_ctx = StyleContext {
|
let style_ctx = StyleContext {
|
||||||
text_style_names: self.text_style_names.clone(),
|
text_style_names: self.text_style_names.clone(),
|
||||||
active_text_style: self.active_text_style.clone(),
|
active_text_style: self.active_text_style.clone(),
|
||||||
|
|
@ -386,92 +376,40 @@ impl Ribbon {
|
||||||
active_table_style: self.active_table_style.clone(),
|
active_table_style: self.active_table_style.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut widgets: Vec<Element<Message>> = Vec::new();
|
// Two density variants: full-size panels, and a compact one where
|
||||||
let mut first_group = true;
|
// large buttons shrink to icon-only columns. DensitySwap shows the
|
||||||
|
// widest that fits the current ribbon width.
|
||||||
for group in groups {
|
let full = build_tool_row(
|
||||||
if !first_group {
|
false,
|
||||||
widgets.push(
|
&groups,
|
||||||
container(text(""))
|
&self.active_tool,
|
||||||
.width(1)
|
&self.open_dropdown,
|
||||||
.height(Fill)
|
&self.last_cmd,
|
||||||
.style(|_: &Theme| container::Style {
|
self.wireframe,
|
||||||
background: Some(Background::Color(BORDER_DARK)),
|
self.ortho_mode,
|
||||||
..Default::default()
|
&self.layer_infos,
|
||||||
})
|
&self.active_layer,
|
||||||
.into(),
|
self.active_color,
|
||||||
);
|
&self.active_linetype,
|
||||||
}
|
self.active_lineweight,
|
||||||
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 = matches!(
|
|
||||||
&item,
|
|
||||||
RibbonItem::LargeTool(_)
|
|
||||||
| RibbonItem::LargeDropdown { .. }
|
|
||||||
| RibbonItem::LayerComboGroup { .. }
|
|
||||||
| RibbonItem::PropertiesGroup { .. }
|
|
||||||
| RibbonItem::StyleComboGroup { .. }
|
|
||||||
);
|
|
||||||
|
|
||||||
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,
|
&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::Fill)
|
|
||||||
.into(),
|
|
||||||
);
|
);
|
||||||
}
|
let compact = build_tool_row(
|
||||||
|
true,
|
||||||
widgets
|
&groups,
|
||||||
.into_iter()
|
&self.active_tool,
|
||||||
.fold(row![].spacing(0).height(Length::Fill), |r, w| r.push(w))
|
&self.open_dropdown,
|
||||||
.into()
|
&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,
|
||||||
|
);
|
||||||
|
DensitySwap::new(vec![full, compact]).into()
|
||||||
} else {
|
} else {
|
||||||
text("").into()
|
text("").into()
|
||||||
};
|
};
|
||||||
|
|
@ -1136,6 +1074,112 @@ impl Ribbon {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Render one ribbon tool row for the active module's groups. When `compact`,
|
||||||
|
/// large tools/dropdowns are drawn as small icon columns so the whole row is
|
||||||
|
/// narrower; the combo groups (layer / properties / style) always stay full.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn build_tool_row<'a>(
|
||||||
|
compact: bool,
|
||||||
|
groups: &[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 widgets: Vec<Element<Message>> = Vec::new();
|
||||||
|
let mut first_group = true;
|
||||||
|
|
||||||
|
for group in groups {
|
||||||
|
if !first_group {
|
||||||
|
widgets.push(
|
||||||
|
container(text(""))
|
||||||
|
.width(1)
|
||||||
|
.height(Fill)
|
||||||
|
.style(|_: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(BORDER_DARK)),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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::Fill)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
widgets
|
||||||
|
.into_iter()
|
||||||
|
.fold(row![].spacing(0).height(Length::Fill), |r, w| r.push(w))
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for Ribbon {
|
impl Default for Ribbon {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
|
|
|
||||||
|
|
@ -344,7 +344,9 @@ pub(super) fn render_small<'a>(
|
||||||
ortho_mode: bool,
|
ortho_mode: bool,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
match item {
|
match item {
|
||||||
RibbonItem::Tool(t) => {
|
// Large variants render small too, so the ribbon can shrink a panel of
|
||||||
|
// large buttons to icon-only columns when the width is tight.
|
||||||
|
RibbonItem::Tool(t) | RibbonItem::LargeTool(t) => {
|
||||||
let active = is_active_tool(t.id, active_tool, wireframe, ortho_mode);
|
let active = is_active_tool(t.id, active_tool, wireframe, ortho_mode);
|
||||||
let event = t.event.clone();
|
let event = t.event.clone();
|
||||||
let tool_id = t.id.to_string();
|
let tool_id = t.id.to_string();
|
||||||
|
|
@ -368,6 +370,13 @@ pub(super) fn render_small<'a>(
|
||||||
items,
|
items,
|
||||||
default,
|
default,
|
||||||
..
|
..
|
||||||
|
}
|
||||||
|
| RibbonItem::LargeDropdown {
|
||||||
|
id,
|
||||||
|
icon,
|
||||||
|
items,
|
||||||
|
default,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
let active = active_tool.as_deref() == Some(*id)
|
let active = active_tool.as_deref() == Some(*id)
|
||||||
|| items
|
|| items
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
//! anchor overlays below the possibly-taller bar.
|
//! anchor overlays below the possibly-taller bar.
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicU32, Ordering};
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
|
use std::cell::Cell;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use iced::advanced::layout::{self, Layout};
|
use iced::advanced::layout::{self, Layout};
|
||||||
|
|
@ -612,3 +613,180 @@ impl<'a> From<WrapFlow<'a>> for Element<'a, Message> {
|
||||||
Element::new(w)
|
Element::new(w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Picks the widest variant that fits the available width and shows only that
|
||||||
|
/// one — used by the ribbon tool area to swap a full-size panel row for a
|
||||||
|
/// compact (icon-only) one when the window is too narrow. Variants must be
|
||||||
|
/// ordered widest-first; the last is shown when none fit.
|
||||||
|
pub struct DensitySwap<'a> {
|
||||||
|
variants: Vec<Element<'a, Message>>,
|
||||||
|
chosen: Cell<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> DensitySwap<'a> {
|
||||||
|
pub fn new(variants: Vec<Element<'a, Message>>) -> Self {
|
||||||
|
Self {
|
||||||
|
variants,
|
||||||
|
chosen: Cell::new(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget<Message, Theme, Renderer> for DensitySwap<'a> {
|
||||||
|
fn children(&self) -> Vec<widget::Tree> {
|
||||||
|
self.variants.iter().map(widget::Tree::new).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&self, tree: &mut widget::Tree) {
|
||||||
|
let refs: Vec<_> = self.variants.iter().map(|e| e.as_widget()).collect();
|
||||||
|
tree.diff_children(&refs);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size::new(Length::Shrink, Length::Fill)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, limits.max().height));
|
||||||
|
|
||||||
|
// Widest-first: keep the first variant whose natural width fits; else the
|
||||||
|
// last (narrowest).
|
||||||
|
let mut pick = self.variants.len().saturating_sub(1);
|
||||||
|
for (i, v) in self.variants.iter_mut().enumerate() {
|
||||||
|
let n = v.as_widget_mut().layout(&mut tree.children[i], renderer, &natural);
|
||||||
|
if n.size().width <= max_w {
|
||||||
|
pick = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.chosen.set(pick);
|
||||||
|
|
||||||
|
let node =
|
||||||
|
self.variants[pick]
|
||||||
|
.as_widget_mut()
|
||||||
|
.layout(&mut tree.children[pick], renderer, limits);
|
||||||
|
let sz = node.size();
|
||||||
|
layout::Node::with_children(sz, vec![node])
|
||||||
|
}
|
||||||
|
|
||||||
|
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 i = self.chosen.get();
|
||||||
|
if let Some(child_layout) = layout.children().next() {
|
||||||
|
self.variants[i].as_widget_mut().update(
|
||||||
|
&mut tree.children[i],
|
||||||
|
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 i = self.chosen.get();
|
||||||
|
layout
|
||||||
|
.children()
|
||||||
|
.next()
|
||||||
|
.map(|child_layout| {
|
||||||
|
self.variants[i].as_widget().mouse_interaction(
|
||||||
|
&tree.children[i],
|
||||||
|
child_layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut widget::Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn widget::Operation,
|
||||||
|
) {
|
||||||
|
let i = self.chosen.get();
|
||||||
|
if let Some(child_layout) = layout.children().next() {
|
||||||
|
self.variants[i]
|
||||||
|
.as_widget_mut()
|
||||||
|
.operate(&mut tree.children[i], 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 i = self.chosen.get();
|
||||||
|
if let Some(child_layout) = layout.children().next() {
|
||||||
|
self.variants[i].as_widget().draw(
|
||||||
|
&tree.children[i],
|
||||||
|
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 i = self.chosen.get();
|
||||||
|
let child_layout = layout.children().next()?;
|
||||||
|
self.variants[i].as_widget_mut().overlay(
|
||||||
|
&mut tree.children[i],
|
||||||
|
child_layout,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<DensitySwap<'a>> for Element<'a, Message> {
|
||||||
|
fn from(w: DensitySwap<'a>) -> Self {
|
||||||
|
Element::new(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue