feat(ribbon): wrap compact panels onto extra rows when still too narrow
DensitySwap now takes a third, narrowest variant: the compact panels flex-wrapped (via WrapFlow) across as many rows as the width needs. The tool area therefore degrades full -> compact -> wrapped as the window shrinks. build_tool_row becomes build_tool_groups, returning fixed-height panel elements so they lay out identically in a row or a wrap flow. The tool bar is no longer a fixed TOOL_BAR_H; DensitySwap reports its measured height into a cell and the ribbon dropdown overlays anchor at tab_bar_height() + tool_bar_height() so they track the taller bar. Second 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
65dfd4e462
commit
ff36a1bdac
2 changed files with 79 additions and 55 deletions
|
|
@ -59,6 +59,9 @@ pub struct Ribbon {
|
||||||
/// Measured tab-bar height (28 on one row, 56 when tabs wrap). Written by
|
/// Measured tab-bar height (28 on one row, 56 when tabs wrap). Written by
|
||||||
/// the `WrapBar` widget during layout, read when anchoring dropdowns.
|
/// the `WrapBar` widget during layout, read when anchoring dropdowns.
|
||||||
tab_bar_h: Arc<AtomicU32>,
|
tab_bar_h: Arc<AtomicU32>,
|
||||||
|
/// Measured tool-area height (TOOL_BAR_H on one row, taller when the panels
|
||||||
|
/// wrap). Written by the `DensitySwap` widget, read when anchoring dropdowns.
|
||||||
|
tool_bar_h: Arc<AtomicU32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-layer display data shown in the ribbon layer dropdown.
|
/// Per-layer display data shown in the ribbon layer dropdown.
|
||||||
|
|
@ -114,6 +117,7 @@ impl Ribbon {
|
||||||
table_style_names: vec![],
|
table_style_names: vec![],
|
||||||
active_table_style: String::new(),
|
active_table_style: String::new(),
|
||||||
tab_bar_h: Arc::new(AtomicU32::new(28.0f32.to_bits())),
|
tab_bar_h: Arc::new(AtomicU32::new(28.0f32.to_bits())),
|
||||||
|
tool_bar_h: Arc::new(AtomicU32::new(TOOL_BAR_H.to_bits())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,6 +126,11 @@ impl Ribbon {
|
||||||
f32::from_bits(self.tab_bar_h.load(Ordering::Relaxed))
|
f32::from_bits(self.tab_bar_h.load(Ordering::Relaxed))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Current tool-area height as last measured by the `DensitySwap` widget.
|
||||||
|
fn tool_bar_height(&self) -> f32 {
|
||||||
|
f32::from_bits(self.tool_bar_h.load(Ordering::Relaxed))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_styles(
|
pub fn set_styles(
|
||||||
&mut self,
|
&mut self,
|
||||||
text: Vec<String>,
|
text: Vec<String>,
|
||||||
|
|
@ -376,11 +385,14 @@ impl Ribbon {
|
||||||
active_table_style: self.active_table_style.clone(),
|
active_table_style: self.active_table_style.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Two density variants: full-size panels, and a compact one where
|
// Adaptive tool area, widest→narrowest:
|
||||||
// large buttons shrink to icon-only columns. DensitySwap shows the
|
// 1. full — full-size panels on one row
|
||||||
// widest that fits the current ribbon width.
|
// 2. compact — large buttons shrink to icon-only columns
|
||||||
let full = build_tool_row(
|
// 3. wrapped — compact panels flex-wrap onto extra rows
|
||||||
false,
|
// DensitySwap shows the widest variant that fits the width.
|
||||||
|
let build = |compact: bool| {
|
||||||
|
build_tool_groups(
|
||||||
|
compact,
|
||||||
&groups,
|
&groups,
|
||||||
&self.active_tool,
|
&self.active_tool,
|
||||||
&self.open_dropdown,
|
&self.open_dropdown,
|
||||||
|
|
@ -393,23 +405,25 @@ impl Ribbon {
|
||||||
&self.active_linetype,
|
&self.active_linetype,
|
||||||
self.active_lineweight,
|
self.active_lineweight,
|
||||||
&style_ctx,
|
&style_ctx,
|
||||||
);
|
)
|
||||||
let compact = build_tool_row(
|
};
|
||||||
true,
|
let full: Element<'_, Message> = build(false)
|
||||||
&groups,
|
.into_iter()
|
||||||
&self.active_tool,
|
.fold(row![].spacing(0).height(Length::Fixed(TOOL_BAR_H)), |r, w| {
|
||||||
&self.open_dropdown,
|
r.push(w)
|
||||||
&self.last_cmd,
|
})
|
||||||
self.wireframe,
|
.into();
|
||||||
self.ortho_mode,
|
let compact: Element<'_, Message> = build(true)
|
||||||
&self.layer_infos,
|
.into_iter()
|
||||||
&self.active_layer,
|
.fold(row![].spacing(0).height(Length::Fixed(TOOL_BAR_H)), |r, w| {
|
||||||
self.active_color,
|
r.push(w)
|
||||||
&self.active_linetype,
|
})
|
||||||
self.active_lineweight,
|
.into();
|
||||||
&style_ctx,
|
let wrapped: Element<'_, Message> =
|
||||||
);
|
WrapFlow::new(build(true)).spacing_x(0.0).row_h(TOOL_BAR_H).into();
|
||||||
DensitySwap::new(vec![full, compact]).into()
|
DensitySwap::new(vec![full, compact, wrapped])
|
||||||
|
.report_height(self.tool_bar_h.clone())
|
||||||
|
.into()
|
||||||
} else {
|
} else {
|
||||||
text("").into()
|
text("").into()
|
||||||
};
|
};
|
||||||
|
|
@ -424,8 +438,7 @@ impl Ribbon {
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.width(Length::Fill)
|
.width(Length::Fill);
|
||||||
.height(TOOL_BAR_H);
|
|
||||||
|
|
||||||
column![tab_bar, tool_bar].into()
|
column![tab_bar, tool_bar].into()
|
||||||
}
|
}
|
||||||
|
|
@ -606,7 +619,7 @@ impl Ribbon {
|
||||||
})
|
})
|
||||||
.width(Length::Fixed(190.0));
|
.width(Length::Fixed(190.0));
|
||||||
|
|
||||||
let top_offset = self.tab_bar_height() + TOOL_BAR_H;
|
let top_offset = self.tab_bar_height() + self.tool_bar_height();
|
||||||
let left_offset = compute_dropdown_left(&groups, open_id);
|
let left_offset = compute_dropdown_left(&groups, open_id);
|
||||||
let positioned = container(panel)
|
let positioned = container(panel)
|
||||||
.align_left(Fill)
|
.align_left(Fill)
|
||||||
|
|
@ -729,7 +742,7 @@ impl Ribbon {
|
||||||
})
|
})
|
||||||
.width(Length::Fixed(220.0));
|
.width(Length::Fixed(220.0));
|
||||||
|
|
||||||
let top_offset = self.tab_bar_height() + TOOL_BAR_H;
|
let top_offset = self.tab_bar_height() + self.tool_bar_height();
|
||||||
let groups = self.modules.get(self.active)?.ribbon_groups();
|
let groups = self.modules.get(self.active)?.ribbon_groups();
|
||||||
let left_offset = compute_layer_combo_left(&groups);
|
let left_offset = compute_layer_combo_left(&groups);
|
||||||
let positioned = container(panel)
|
let positioned = container(panel)
|
||||||
|
|
@ -853,7 +866,7 @@ impl Ribbon {
|
||||||
})
|
})
|
||||||
.width(Length::Fixed(LARGE_W * 2.3));
|
.width(Length::Fixed(LARGE_W * 2.3));
|
||||||
|
|
||||||
let top_offset = self.tab_bar_height() + TOOL_BAR_H;
|
let top_offset = self.tab_bar_height() + self.tool_bar_height();
|
||||||
let left_offset = compute_dropdown_left(&groups, open_id);
|
let left_offset = compute_dropdown_left(&groups, open_id);
|
||||||
let positioned = container(panel)
|
let positioned = container(panel)
|
||||||
.align_left(Fill)
|
.align_left(Fill)
|
||||||
|
|
@ -891,7 +904,7 @@ impl Ribbon {
|
||||||
})
|
})
|
||||||
.width(Length::Fixed(200.0));
|
.width(Length::Fixed(200.0));
|
||||||
|
|
||||||
let top_offset = self.tab_bar_height() + TOOL_BAR_H;
|
let top_offset = self.tab_bar_height() + self.tool_bar_height();
|
||||||
let groups = self.modules.get(self.active)?.ribbon_groups();
|
let groups = self.modules.get(self.active)?.ribbon_groups();
|
||||||
let left_offset = compute_prop_combo_left(&groups, PROP_COLOR_ID);
|
let left_offset = compute_prop_combo_left(&groups, PROP_COLOR_ID);
|
||||||
let positioned = container(panel)
|
let positioned = container(panel)
|
||||||
|
|
@ -980,7 +993,7 @@ impl Ribbon {
|
||||||
})
|
})
|
||||||
.width(Length::Fixed(220.0));
|
.width(Length::Fixed(220.0));
|
||||||
|
|
||||||
let top_offset = self.tab_bar_height() + TOOL_BAR_H;
|
let top_offset = self.tab_bar_height() + self.tool_bar_height();
|
||||||
let groups = self.modules.get(self.active)?.ribbon_groups();
|
let groups = self.modules.get(self.active)?.ribbon_groups();
|
||||||
let left_offset = compute_prop_combo_left(&groups, PROP_LINETYPE_ID);
|
let left_offset = compute_prop_combo_left(&groups, PROP_LINETYPE_ID);
|
||||||
let positioned = container(list)
|
let positioned = container(list)
|
||||||
|
|
@ -1056,7 +1069,7 @@ impl Ribbon {
|
||||||
})
|
})
|
||||||
.width(Length::Fixed(width));
|
.width(Length::Fixed(width));
|
||||||
|
|
||||||
let top_offset = self.tab_bar_height() + TOOL_BAR_H;
|
let top_offset = self.tab_bar_height() + self.tool_bar_height();
|
||||||
let groups = self.modules.get(self.active)?.ribbon_groups();
|
let groups = self.modules.get(self.active)?.ribbon_groups();
|
||||||
let left_offset = compute_prop_combo_left(&groups, dd_id);
|
let left_offset = compute_prop_combo_left(&groups, dd_id);
|
||||||
let positioned = container(panel)
|
let positioned = container(panel)
|
||||||
|
|
@ -1074,11 +1087,13 @@ impl Ribbon {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render one ribbon tool row for the active module's groups. When `compact`,
|
/// Render the active module's ribbon panels as a list of group elements (with
|
||||||
/// large tools/dropdowns are drawn as small icon columns so the whole row is
|
/// 1px dividers between them). When `compact`, large tools/dropdowns are drawn
|
||||||
/// narrower; the combo groups (layer / properties / style) always stay full.
|
/// as small icon columns so each panel is narrower; the combo groups (layer /
|
||||||
|
/// properties / style) always stay full. Each panel is a fixed `TOOL_BAR_H`
|
||||||
|
/// tall so the list can be laid out in a row or flex-wrapped across rows.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn build_tool_row<'a>(
|
fn build_tool_groups<'a>(
|
||||||
compact: bool,
|
compact: bool,
|
||||||
groups: &[RibbonGroup],
|
groups: &[RibbonGroup],
|
||||||
active_tool: &Option<String>,
|
active_tool: &Option<String>,
|
||||||
|
|
@ -1092,7 +1107,7 @@ fn build_tool_row<'a>(
|
||||||
active_linetype: &'a str,
|
active_linetype: &'a str,
|
||||||
active_lineweight: LineWeight,
|
active_lineweight: LineWeight,
|
||||||
style_ctx: &StyleContext,
|
style_ctx: &StyleContext,
|
||||||
) -> Element<'a, Message> {
|
) -> Vec<Element<'a, Message>> {
|
||||||
let mut widgets: Vec<Element<Message>> = Vec::new();
|
let mut widgets: Vec<Element<Message>> = Vec::new();
|
||||||
let mut first_group = true;
|
let mut first_group = true;
|
||||||
|
|
||||||
|
|
@ -1101,7 +1116,7 @@ fn build_tool_row<'a>(
|
||||||
widgets.push(
|
widgets.push(
|
||||||
container(text(""))
|
container(text(""))
|
||||||
.width(1)
|
.width(1)
|
||||||
.height(Fill)
|
.height(Length::Fixed(TOOL_BAR_H))
|
||||||
.style(|_: &Theme| container::Style {
|
.style(|_: &Theme| container::Style {
|
||||||
background: Some(Background::Color(BORDER_DARK)),
|
background: Some(Background::Color(BORDER_DARK)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
@ -1169,15 +1184,12 @@ fn build_tool_row<'a>(
|
||||||
.align_x(iced::Center)
|
.align_x(iced::Center)
|
||||||
.spacing(0)
|
.spacing(0)
|
||||||
.padding([3u16, 4])
|
.padding([3u16, 4])
|
||||||
.height(Length::Fill)
|
.height(Length::Fixed(TOOL_BAR_H))
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
widgets
|
widgets
|
||||||
.into_iter()
|
|
||||||
.fold(row![].spacing(0).height(Length::Fill), |r, w| r.push(w))
|
|
||||||
.into()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Ribbon {
|
impl Default for Ribbon {
|
||||||
|
|
|
||||||
|
|
@ -621,6 +621,7 @@ impl<'a> From<WrapFlow<'a>> for Element<'a, Message> {
|
||||||
pub struct DensitySwap<'a> {
|
pub struct DensitySwap<'a> {
|
||||||
variants: Vec<Element<'a, Message>>,
|
variants: Vec<Element<'a, Message>>,
|
||||||
chosen: Cell<usize>,
|
chosen: Cell<usize>,
|
||||||
|
height_out: Option<Arc<AtomicU32>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> DensitySwap<'a> {
|
impl<'a> DensitySwap<'a> {
|
||||||
|
|
@ -628,8 +629,16 @@ impl<'a> DensitySwap<'a> {
|
||||||
Self {
|
Self {
|
||||||
variants,
|
variants,
|
||||||
chosen: Cell::new(0),
|
chosen: Cell::new(0),
|
||||||
|
height_out: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Report the chosen variant's height (bits of an `f32`) so callers can
|
||||||
|
/// anchor overlays below a possibly-taller (wrapped) tool area.
|
||||||
|
pub fn report_height(mut self, out: Arc<AtomicU32>) -> Self {
|
||||||
|
self.height_out = Some(out);
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Widget<Message, Theme, Renderer> for DensitySwap<'a> {
|
impl<'a> Widget<Message, Theme, Renderer> for DensitySwap<'a> {
|
||||||
|
|
@ -643,7 +652,7 @@ impl<'a> Widget<Message, Theme, Renderer> for DensitySwap<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn size(&self) -> Size<Length> {
|
fn size(&self) -> Size<Length> {
|
||||||
Size::new(Length::Shrink, Length::Fill)
|
Size::new(Length::Shrink, Length::Shrink)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn layout(
|
fn layout(
|
||||||
|
|
@ -654,10 +663,10 @@ impl<'a> Widget<Message, Theme, Renderer> for DensitySwap<'a> {
|
||||||
) -> layout::Node {
|
) -> layout::Node {
|
||||||
let max_w = limits.max().width;
|
let max_w = limits.max().width;
|
||||||
let natural =
|
let natural =
|
||||||
layout::Limits::new(Size::ZERO, Size::new(f32::INFINITY, limits.max().height));
|
layout::Limits::new(Size::ZERO, Size::new(f32::INFINITY, f32::INFINITY));
|
||||||
|
|
||||||
// Widest-first: keep the first variant whose natural width fits; else the
|
// Widest-first: keep the first variant whose natural width fits; else the
|
||||||
// last (narrowest).
|
// last (which is expected to wrap to fit any width).
|
||||||
let mut pick = self.variants.len().saturating_sub(1);
|
let mut pick = self.variants.len().saturating_sub(1);
|
||||||
for (i, v) in self.variants.iter_mut().enumerate() {
|
for (i, v) in self.variants.iter_mut().enumerate() {
|
||||||
let n = v.as_widget_mut().layout(&mut tree.children[i], renderer, &natural);
|
let n = v.as_widget_mut().layout(&mut tree.children[i], renderer, &natural);
|
||||||
|
|
@ -673,6 +682,9 @@ impl<'a> Widget<Message, Theme, Renderer> for DensitySwap<'a> {
|
||||||
.as_widget_mut()
|
.as_widget_mut()
|
||||||
.layout(&mut tree.children[pick], renderer, limits);
|
.layout(&mut tree.children[pick], renderer, limits);
|
||||||
let sz = node.size();
|
let sz = node.size();
|
||||||
|
if let Some(out) = &self.height_out {
|
||||||
|
out.store(sz.height.to_bits(), Ordering::Relaxed);
|
||||||
|
}
|
||||||
layout::Node::with_children(sz, vec![node])
|
layout::Node::with_children(sz, vec![node])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue