forked from 1b-agent/opencadstudio-upstream
feat(ribbon): fit tabs and collapse panels to dropdowns on narrow screens
Two narrow-mode (e.g. phone-width) ribbon fixes; both are no-ops when the row has room, so desktop layouts are unchanged. - Tab row: let the inter-tab gap squeeze from 6px down to -12px, tucking neighbouring tabs into each other's 14px side padding (labels never overlap) so all tabs stay on one row instead of wrapping "Manage" onto a second line. The gap only goes negative as far as needed to fit. - Tool panels: at the tightest collapse step each panel is now a single non-running button — the panel's first tool icon + title + a down arrow that opens the tools flyout. No tool runs directly at this density; you pick one from the dropdown. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
0cd861cc76
commit
d38fcda07c
1 file changed
+110
-70
+110
-70
@@ -449,10 +449,13 @@ impl Ribbon {
|
||||
},
|
||||
);
|
||||
|
||||
// Tabs may squeeze their gaps shut (down to 0px) to fit before wrapping.
|
||||
// Tabs may squeeze their gaps to fit before wrapping: from the normal 6px
|
||||
// down to -12px on a narrow (e.g. phone) tab row, tucking neighbours into
|
||||
// each other's 14px side padding without overlapping the labels. When the
|
||||
// row has room the gap stays 6px, so wide/desktop layouts are unchanged.
|
||||
let tabs = WrapFlow::new(tab_items)
|
||||
.spacing_x(6.0)
|
||||
.min_spacing_x(0.0)
|
||||
.min_spacing_x(-12.0)
|
||||
.row_h(28.0);
|
||||
|
||||
// Panel-density selector, pinned to the right edge of the tab row: a bare
|
||||
@@ -1382,10 +1385,26 @@ fn representative<'g>(group: &'g RibbonGroup, last_used: Option<&str>) -> Option
|
||||
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. Its height is its
|
||||
/// natural content height (shorter than a full 3-row panel), so `CollapsePanels`
|
||||
/// can shrink the ribbon once every panel is collapsed.
|
||||
/// The icon of a panel's first tool-like item. Shown on the tightest collapse
|
||||
/// button; digs into the composite groups so Layers/Properties/Styles panels
|
||||
/// still get a representative icon.
|
||||
fn first_tool_icon(group: &RibbonGroup) -> Option<IconKind> {
|
||||
group.tools.iter().find_map(|it| match it {
|
||||
RibbonItem::Tool(t) | RibbonItem::LargeTool(t) => Some(t.icon),
|
||||
RibbonItem::Dropdown { icon, .. } | RibbonItem::LargeDropdown { icon, .. } => Some(*icon),
|
||||
RibbonItem::PropertiesGroup { match_prop } => Some(match_prop.icon),
|
||||
RibbonItem::LayerComboGroup { row2, .. } => row2.first().map(|t| t.icon),
|
||||
RibbonItem::StyleComboGroup { rows, .. } => {
|
||||
rows.first().and_then(|r| r.first()).map(|t| t.icon)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A collapsed panel. In the COLLAPSED step: a live representative-tool button
|
||||
/// (updates to the last-used tool) plus a title + ▾ opener for the full flyout.
|
||||
/// In the tighter `compact` (TIGHT) step: just the first tool's icon + ▾, a pure
|
||||
/// flyout opener with no direct tool run. Height is natural content height
|
||||
/// (shorter than a full 3-row panel), so `CollapsePanels` can shrink the ribbon.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn collapse_button<'a>(
|
||||
group: &RibbonGroup,
|
||||
@@ -1401,65 +1420,91 @@ fn collapse_button<'a>(
|
||||
active_linetype: &'a str,
|
||||
active_lineweight: LineWeight,
|
||||
style_ctx: &StyleContext,
|
||||
// When set, the button shows a SMALL representative icon instead of the large
|
||||
// one — the `tight` step used once even the all-collapsed row overflows.
|
||||
// When set, this is the TIGHT step (reached once even the all-collapsed row
|
||||
// overflows): the whole panel becomes a single non-running button — the first
|
||||
// tool's icon + a ▾ that opens the tools flyout. Nothing runs directly here.
|
||||
compact: bool,
|
||||
) -> Element<'a, Message> {
|
||||
let title = group.title;
|
||||
|
||||
// The representative tool's face. Normally a large icon; in the tight step it
|
||||
// shrinks to the small icon (SMALL_W vs LARGE_W) to reclaim width. For a
|
||||
// Properties panel the representative is its Match button.
|
||||
// Tightest form: one button = the panel's FIRST tool icon + its title + ▾.
|
||||
// Clicking opens the flyout listing every tool; no tool runs directly at this
|
||||
// density (you pick one from the dropdown), so the tightest row is pure openers.
|
||||
if compact {
|
||||
let icon: Element<'_, Message> = match first_tool_icon(group) {
|
||||
Some(ik) => make_icon(ik, SMALL_ICON),
|
||||
None => text("").into(),
|
||||
};
|
||||
return button(
|
||||
column![
|
||||
icon,
|
||||
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),
|
||||
]
|
||||
.align_x(iced::Center)
|
||||
.spacing(2),
|
||||
)
|
||||
.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([3, 5])
|
||||
.into();
|
||||
}
|
||||
|
||||
// Collapsed (not yet tight): a large representative-tool face — a live button
|
||||
// that runs the last-used tool — above a title + ▾ opener for the full flyout.
|
||||
// For a Properties panel the representative is its Match button.
|
||||
let rep = representative(group, last_used);
|
||||
let face: Element<'_, Message> = if compact {
|
||||
match rep {
|
||||
Some(RibbonItem::PropertiesGroup { match_prop }) => render_small(
|
||||
&RibbonItem::LargeTool(match_prop.clone()),
|
||||
active_tool,
|
||||
open_dd,
|
||||
last_cmd,
|
||||
wireframe,
|
||||
ortho_mode,
|
||||
),
|
||||
Some(item) => {
|
||||
render_small(item, active_tool, open_dd, last_cmd, wireframe, ortho_mode)
|
||||
}
|
||||
None => text("").into(),
|
||||
}
|
||||
} else {
|
||||
match rep {
|
||||
Some(RibbonItem::PropertiesGroup { match_prop }) => render_large(
|
||||
&RibbonItem::LargeTool(match_prop.clone()),
|
||||
active_tool,
|
||||
open_dd,
|
||||
last_cmd,
|
||||
wireframe,
|
||||
ortho_mode,
|
||||
layer_infos,
|
||||
active_layer,
|
||||
active_color,
|
||||
active_linetype,
|
||||
active_lineweight,
|
||||
style_ctx,
|
||||
false,
|
||||
),
|
||||
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,
|
||||
false,
|
||||
),
|
||||
None => text("").into(),
|
||||
}
|
||||
let face: Element<'_, Message> = match rep {
|
||||
Some(RibbonItem::PropertiesGroup { match_prop }) => render_large(
|
||||
&RibbonItem::LargeTool(match_prop.clone()),
|
||||
active_tool,
|
||||
open_dd,
|
||||
last_cmd,
|
||||
wireframe,
|
||||
ortho_mode,
|
||||
layer_infos,
|
||||
active_layer,
|
||||
active_color,
|
||||
active_linetype,
|
||||
active_lineweight,
|
||||
style_ctx,
|
||||
false,
|
||||
),
|
||||
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,
|
||||
false,
|
||||
),
|
||||
None => text("").into(),
|
||||
};
|
||||
|
||||
let opener = button(
|
||||
@@ -1489,16 +1534,11 @@ fn collapse_button<'a>(
|
||||
})
|
||||
.padding([1, 4]);
|
||||
|
||||
// The large face fills a fixed slot; the small (compact) face keeps its own
|
||||
// natural height. Either way the whole button is natural height, so a
|
||||
// collapsed / tight panel is shorter than a full one and the row shrinks.
|
||||
let face_box = if compact {
|
||||
container(face).align_y(iced::Center)
|
||||
} else {
|
||||
container(face)
|
||||
.height(Length::Fixed(COLLAPSED_FACE_H))
|
||||
.align_y(iced::Center)
|
||||
};
|
||||
// The large face fills a fixed slot so a collapsed panel is shorter than a full
|
||||
// 3-row panel, letting `CollapsePanels` shrink the ribbon row.
|
||||
let face_box = container(face)
|
||||
.height(Length::Fixed(COLLAPSED_FACE_H))
|
||||
.align_y(iced::Center);
|
||||
|
||||
column![face_box, opener]
|
||||
.align_x(iced::Center)
|
||||
|
||||
Reference in new issue
Block a user