feat(ui): flex-wrap the ribbon top bar and status bar when narrow
Add shared adaptive-bar widgets in src/ui/wrap_bar.rs: - WrapBar: two-block bar (lead + optional Fill middle + trail). Fits on one row, otherwise the trailing block drops below; with no middle both blocks flex-wrap in their own row bands and stack so a wrapped lead item never shares a row with a trailing item. - WrapFlow: CSS-style flex-wrap row that spreads its items across as many rows as the width needs. Ribbon tab row: quick-access buttons and undo/redo are merged into one WrapFlow; the tabs are a second WrapFlow. Dropdown overlays anchor to the measured (possibly taller) bar height instead of a hardcoded 28px. Status bar: the hamburger menu plus the Model/layout tabs form one WrapFlow; the status pills form another. The layout-tab scroll arrows and horizontal scrollable are removed in favour of wrapping. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a7ad7e078a
commit
9e1b997631
4 changed files with 814 additions and 181 deletions
|
|
@ -15,6 +15,7 @@ pub mod statusbar;
|
||||||
pub mod style;
|
pub mod style;
|
||||||
pub mod text_util;
|
pub mod text_util;
|
||||||
pub mod window;
|
pub mod window;
|
||||||
|
pub mod wrap_bar;
|
||||||
|
|
||||||
pub use command_line::CommandLine;
|
pub use command_line::CommandLine;
|
||||||
pub use properties::PropertiesPanel;
|
pub use properties::PropertiesPanel;
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@
|
||||||
//
|
//
|
||||||
// Dropdown items within a group are collected into columns of 3 rows.
|
// Dropdown items within a group are collected into columns of 3 rows.
|
||||||
|
|
||||||
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use rustc_hash::FxHashMap as HashMap;
|
use rustc_hash::FxHashMap as HashMap;
|
||||||
|
|
||||||
use acadrust::types::{Color as AcadColor, LineWeight};
|
use acadrust::types::{Color as AcadColor, LineWeight};
|
||||||
|
|
@ -19,6 +22,7 @@ use crate::ui::properties::{lw_options, LinetypeItem};
|
||||||
|
|
||||||
mod widgets;
|
mod widgets;
|
||||||
use widgets::{StyleContext, *};
|
use widgets::{StyleContext, *};
|
||||||
|
use crate::ui::wrap_bar::{WrapBar, WrapFlow};
|
||||||
|
|
||||||
// ── Ribbon state ───────────────────────────────────────────────────────────
|
// ── Ribbon state ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -52,6 +56,9 @@ pub struct Ribbon {
|
||||||
pub active_mleader_style: String,
|
pub active_mleader_style: String,
|
||||||
pub table_style_names: Vec<String>,
|
pub table_style_names: Vec<String>,
|
||||||
pub active_table_style: String,
|
pub active_table_style: String,
|
||||||
|
/// Measured tab-bar height (28 on one row, 56 when tabs wrap). Written by
|
||||||
|
/// the `WrapBar` widget during layout, read when anchoring dropdowns.
|
||||||
|
tab_bar_h: Arc<AtomicU32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-layer display data shown in the ribbon layer dropdown.
|
/// Per-layer display data shown in the ribbon layer dropdown.
|
||||||
|
|
@ -106,9 +113,15 @@ impl Ribbon {
|
||||||
active_mleader_style: String::new(),
|
active_mleader_style: String::new(),
|
||||||
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())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Current tab-bar height as last measured by the `WrapBar` widget.
|
||||||
|
fn tab_bar_height(&self) -> f32 {
|
||||||
|
f32::from_bits(self.tab_bar_h.load(Ordering::Relaxed))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_styles(
|
pub fn set_styles(
|
||||||
&mut self,
|
&mut self,
|
||||||
text: Vec<String>,
|
text: Vec<String>,
|
||||||
|
|
@ -217,34 +230,29 @@ impl Ribbon {
|
||||||
undo_count: usize,
|
undo_count: usize,
|
||||||
redo_count: usize,
|
redo_count: usize,
|
||||||
) -> Element<'_, Message> {
|
) -> Element<'_, Message> {
|
||||||
// ── Quick-access file commands ─────────────────────────────────────
|
// ── Quick-access file commands + undo/redo, one merged flow ────────
|
||||||
let quick_access = row![
|
let lead = WrapFlow::new(vec![
|
||||||
quick_access_btn(crate::ui::icons::DOC_NEW, "New", "NEW"),
|
quick_access_btn(crate::ui::icons::DOC_NEW, "New", "NEW").into(),
|
||||||
quick_access_btn(crate::ui::icons::FOLDER_OPEN, "Open", "OPEN"),
|
quick_access_btn(crate::ui::icons::FOLDER_OPEN, "Open", "OPEN").into(),
|
||||||
quick_access_btn(crate::ui::icons::SAVE, "Save", "SAVE"),
|
quick_access_btn(crate::ui::icons::SAVE, "Save", "SAVE").into(),
|
||||||
quick_access_btn(crate::ui::icons::FILE_EXPORT, "Save As", "SAVEAS"),
|
quick_access_btn(crate::ui::icons::FILE_EXPORT, "Save As", "SAVEAS").into(),
|
||||||
quick_access_btn(crate::ui::icons::PRINT, "Print", "PRINT"),
|
quick_access_btn(crate::ui::icons::PRINT, "Print", "PRINT").into(),
|
||||||
]
|
render_history_control("Undo", UNDO_HISTORY_ID, undo_count, &self.open_dropdown).into(),
|
||||||
.spacing(TOP_HIST_GAP)
|
render_history_control("Redo", REDO_HISTORY_ID, redo_count, &self.open_dropdown).into(),
|
||||||
.align_y(iced::Center);
|
])
|
||||||
|
.spacing_x(TOP_HIST_GAP)
|
||||||
|
.row_h(28.0);
|
||||||
|
|
||||||
// ── Tab buttons ───────────────────────────────────────────────────
|
// The quick-access flow and the tabs flow each flex-wrap; WrapBar stacks
|
||||||
let history_controls = row![
|
// them so a wrapped tab never shares a row with a quick-access button.
|
||||||
render_history_control("Undo", UNDO_HISTORY_ID, undo_count, &self.open_dropdown),
|
|
||||||
render_history_control("Redo", REDO_HISTORY_ID, redo_count, &self.open_dropdown),
|
|
||||||
]
|
|
||||||
.spacing(TOP_HIST_GAP)
|
|
||||||
.align_y(iced::Center);
|
|
||||||
|
|
||||||
let tab_buttons = self.modules.iter().enumerate().fold(
|
let tab_items = self.modules.iter().enumerate().fold(
|
||||||
row![quick_access, history_controls]
|
Vec::<Element<'_, Message>>::new(),
|
||||||
.align_y(iced::Center)
|
|mut acc, (i, module)| {
|
||||||
.spacing(6),
|
|
||||||
|row_acc, (i, module)| {
|
|
||||||
// The Layout module no longer has a ribbon tab — its paper-space
|
// The Layout module no longer has a ribbon tab — its paper-space
|
||||||
// tools live in the right-edge side toolbar (see ui::side_toolbar).
|
// tools live in the right-edge side toolbar (see ui::side_toolbar).
|
||||||
if module.id() == "layout" {
|
if module.id() == "layout" {
|
||||||
return row_acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_active = i == self.active;
|
let is_active = i == self.active;
|
||||||
|
|
@ -324,17 +332,23 @@ impl Ribbon {
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
row_acc.push(btn)
|
acc.push(btn.into());
|
||||||
|
acc
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let tab_bar = container(tab_buttons)
|
let tabs = WrapFlow::new(tab_items).spacing_x(6.0).row_h(28.0);
|
||||||
.style(|_: &Theme| container::Style {
|
|
||||||
background: Some(Background::Color(TOPBAR_BG)),
|
let tab_bar = container(
|
||||||
..Default::default()
|
WrapBar::new(lead.into(), tabs.into())
|
||||||
})
|
.spacing(6.0)
|
||||||
.width(Length::Fill)
|
.report_height(self.tab_bar_h.clone()),
|
||||||
.height(28);
|
)
|
||||||
|
.style(|_: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(TOPBAR_BG)),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.width(Length::Fill);
|
||||||
|
|
||||||
// ── Tool area ─────────────────────────────────────────────────────
|
// ── Tool area ─────────────────────────────────────────────────────
|
||||||
let effective_active = if !is_paper
|
let effective_active = if !is_paper
|
||||||
|
|
@ -654,7 +668,7 @@ impl Ribbon {
|
||||||
})
|
})
|
||||||
.width(Length::Fixed(190.0));
|
.width(Length::Fixed(190.0));
|
||||||
|
|
||||||
let top_offset = 28.0 + TOOL_BAR_H;
|
let top_offset = self.tab_bar_height() + TOOL_BAR_H;
|
||||||
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)
|
||||||
|
|
@ -777,7 +791,7 @@ impl Ribbon {
|
||||||
})
|
})
|
||||||
.width(Length::Fixed(220.0));
|
.width(Length::Fixed(220.0));
|
||||||
|
|
||||||
let top_offset = 28.0 + TOOL_BAR_H;
|
let top_offset = self.tab_bar_height() + TOOL_BAR_H;
|
||||||
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)
|
||||||
|
|
@ -901,7 +915,7 @@ impl Ribbon {
|
||||||
})
|
})
|
||||||
.width(Length::Fixed(LARGE_W * 2.3));
|
.width(Length::Fixed(LARGE_W * 2.3));
|
||||||
|
|
||||||
let top_offset = 28.0 + TOOL_BAR_H;
|
let top_offset = self.tab_bar_height() + TOOL_BAR_H;
|
||||||
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)
|
||||||
|
|
@ -939,7 +953,7 @@ impl Ribbon {
|
||||||
})
|
})
|
||||||
.width(Length::Fixed(200.0));
|
.width(Length::Fixed(200.0));
|
||||||
|
|
||||||
let top_offset = 28.0 + TOOL_BAR_H;
|
let top_offset = self.tab_bar_height() + TOOL_BAR_H;
|
||||||
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)
|
||||||
|
|
@ -1028,7 +1042,7 @@ impl Ribbon {
|
||||||
})
|
})
|
||||||
.width(Length::Fixed(220.0));
|
.width(Length::Fixed(220.0));
|
||||||
|
|
||||||
let top_offset = 28.0 + TOOL_BAR_H;
|
let top_offset = self.tab_bar_height() + TOOL_BAR_H;
|
||||||
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)
|
||||||
|
|
@ -1104,7 +1118,7 @@ impl Ribbon {
|
||||||
})
|
})
|
||||||
.width(Length::Fixed(width));
|
.width(Length::Fixed(width));
|
||||||
|
|
||||||
let top_offset = 28.0 + TOOL_BAR_H;
|
let top_offset = self.tab_bar_height() + TOOL_BAR_H;
|
||||||
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)
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,15 @@ pub mod statusbar_config;
|
||||||
pub mod statusbar_menu;
|
pub mod statusbar_menu;
|
||||||
|
|
||||||
use iced::widget::tooltip::Position as TipPos;
|
use iced::widget::tooltip::Position as TipPos;
|
||||||
use iced::widget::scrollable::{Direction, Scrollbar};
|
|
||||||
use iced::widget::{
|
use iced::widget::{
|
||||||
button, column, container, mouse_area, row, scrollable, text, text_input, tooltip, Row,
|
button, column, container, mouse_area, row, text, text_input, tooltip,
|
||||||
};
|
};
|
||||||
use iced::{Background, Border, Color, Element, Length, Theme};
|
use iced::{Background, Border, Color, Element, Length, Theme};
|
||||||
|
|
||||||
/// Scrollable id of the status-bar layout-tab strip, so the ‹ › arrows (and
|
/// Scrollable id of the status-bar layout-tab strip (retained so the existing
|
||||||
/// `Message::ScrollLayoutTabs`) can scroll it.
|
/// `Message::ScrollLayoutTabs` handler still resolves; the strip now flex-wraps
|
||||||
|
/// instead of scrolling).
|
||||||
pub const LAYOUT_TABS_SCROLL_ID: &str = "statusbar-layout-tabs";
|
pub const LAYOUT_TABS_SCROLL_ID: &str = "statusbar-layout-tabs";
|
||||||
/// How far one ‹ / › arrow click scrolls the layout-tab strip.
|
|
||||||
const LAYOUT_TAB_SCROLL_STEP: f32 = 96.0;
|
|
||||||
|
|
||||||
/// Widget id of the inline layout-rename text input, so the rename can grab
|
/// Widget id of the inline layout-rename text input, so the rename can grab
|
||||||
/// keyboard focus the moment it opens (issue #86).
|
/// keyboard focus the moment it opens (issue #86).
|
||||||
|
|
@ -23,6 +21,7 @@ pub const LAYOUT_RENAME_INPUT_ID: &str = "layout_rename_input";
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::snap::Snapper;
|
use crate::snap::Snapper;
|
||||||
use crate::ui::statusbar::statusbar_config::{StatusBarConfig, StatusPill};
|
use crate::ui::statusbar::statusbar_config::{StatusBarConfig, StatusPill};
|
||||||
|
use crate::ui::wrap_bar::{WrapBar, WrapFlow};
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct StatusBar {
|
pub struct StatusBar {
|
||||||
|
|
@ -137,151 +136,189 @@ impl StatusBar {
|
||||||
status_pill(scale_label).into()
|
status_pill(scale_label).into()
|
||||||
};
|
};
|
||||||
// Build the right-side pills, honouring the user's per-pill visibility.
|
// Build the right-side pills, honouring the user's per-pill visibility.
|
||||||
|
// They live in a flex-wrap flow (WrapFlow) so they spill onto extra rows
|
||||||
|
// when the width can't hold them all on one line.
|
||||||
let vis = |p: StatusPill| config.is_visible(p);
|
let vis = |p: StatusPill| config.is_visible(p);
|
||||||
let mut right_status = Row::new().spacing(2).align_y(iced::Center);
|
let mut pills: Vec<Element<'_, Message>> = Vec::new();
|
||||||
if vis(StatusPill::Coords) {
|
if vis(StatusPill::Coords) {
|
||||||
right_status = right_status.push(tip(
|
pills.push(
|
||||||
status_pill(format_coords(cursor_world)).into(),
|
tip(
|
||||||
"Cursor coordinates (X, Y, Z)",
|
status_pill(format_coords(cursor_world)).into(),
|
||||||
));
|
"Cursor coordinates (X, Y, Z)",
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if vis(StatusPill::Ortho) {
|
if vis(StatusPill::Ortho) {
|
||||||
right_status = right_status.push(tip(
|
pills.push(
|
||||||
toggle_pill(crate::ui::icons::ST_ORTHO, ortho_mode, Message::ToggleOrtho),
|
tip(
|
||||||
"Orthogonal Mode\nF8",
|
toggle_pill(crate::ui::icons::ST_ORTHO, ortho_mode, Message::ToggleOrtho),
|
||||||
));
|
"Orthogonal Mode\nF8",
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if vis(StatusPill::Lwt) {
|
if vis(StatusPill::Lwt) {
|
||||||
right_status = right_status.push(tip(
|
pills.push(
|
||||||
toggle_pill(crate::ui::icons::ST_LWT, lineweight_display, Message::ToggleLineweightDisplay),
|
tip(
|
||||||
"Show Lineweight\nLWDISPLAY",
|
toggle_pill(crate::ui::icons::ST_LWT, lineweight_display, Message::ToggleLineweightDisplay),
|
||||||
));
|
"Show Lineweight\nLWDISPLAY",
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if vis(StatusPill::Polar) {
|
if vis(StatusPill::Polar) {
|
||||||
right_status = right_status.push(polar_pill(polar_mode, polar_increment_deg));
|
pills.push(polar_pill(polar_mode, polar_increment_deg).into());
|
||||||
}
|
}
|
||||||
if vis(StatusPill::Dyn) {
|
if vis(StatusPill::Dyn) {
|
||||||
right_status = right_status.push(tip(
|
pills.push(
|
||||||
toggle_pill(crate::ui::icons::ST_DYN, dyn_input, Message::ToggleDynInput),
|
tip(
|
||||||
"Dynamic Input\nF12",
|
toggle_pill(crate::ui::icons::ST_DYN, dyn_input, Message::ToggleDynInput),
|
||||||
));
|
"Dynamic Input\nF12",
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if vis(StatusPill::Otrack) {
|
if vis(StatusPill::Otrack) {
|
||||||
right_status = right_status.push(tip(
|
pills.push(
|
||||||
toggle_pill(crate::ui::icons::ST_OTRACK, otrack, Message::ToggleOTrack),
|
tip(
|
||||||
"Object Snap Tracking\nF11",
|
toggle_pill(crate::ui::icons::ST_OTRACK, otrack, Message::ToggleOTrack),
|
||||||
));
|
"Object Snap Tracking\nF11",
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if vis(StatusPill::Osnap) {
|
if vis(StatusPill::Osnap) {
|
||||||
right_status =
|
pills.push(osnap_btn(osnap_active, snapper.snap_enabled, popup_open).into());
|
||||||
right_status.push(osnap_btn(osnap_active, snapper.snap_enabled, popup_open));
|
|
||||||
}
|
}
|
||||||
if vis(StatusPill::Space) {
|
if vis(StatusPill::Space) {
|
||||||
right_status = right_status.push(tip(
|
pills.push(
|
||||||
space_mode_btn(¤t_layout, in_mspace),
|
tip(
|
||||||
"PAPER: double-click viewport to enter MSPACE\nMODEL: click to switch to Model Space",
|
space_mode_btn(¤t_layout, in_mspace),
|
||||||
));
|
"PAPER: double-click viewport to enter MSPACE\nMODEL: click to switch to Model Space",
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if vis(StatusPill::Scale) {
|
if vis(StatusPill::Scale) {
|
||||||
right_status = right_status.push(scale_element);
|
pills.push(scale_element);
|
||||||
}
|
}
|
||||||
if vis(StatusPill::Units) {
|
if vis(StatusPill::Units) {
|
||||||
right_status = right_status.push(tip(
|
pills.push(
|
||||||
units_btn(
|
tip(
|
||||||
crate::ui::popup::units_popup::unit_short(insertion_units),
|
units_btn(
|
||||||
units_popup_open,
|
crate::ui::popup::units_popup::unit_short(insertion_units),
|
||||||
),
|
units_popup_open,
|
||||||
"Drawing Units (INSUNITS)\nClick to change",
|
),
|
||||||
));
|
"Drawing Units (INSUNITS)\nClick to change",
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if vis(StatusPill::Transparency) {
|
if vis(StatusPill::Transparency) {
|
||||||
right_status = right_status.push(tip(
|
pills.push(
|
||||||
toggle_pill(
|
tip(
|
||||||
crate::ui::icons::ST_TRANSPARENCY,
|
toggle_pill(
|
||||||
transparency_display,
|
crate::ui::icons::ST_TRANSPARENCY,
|
||||||
Message::ToggleTransparencyDisplay,
|
transparency_display,
|
||||||
),
|
Message::ToggleTransparencyDisplay,
|
||||||
"Show Transparency\nForce opaque when off",
|
),
|
||||||
));
|
"Show Transparency\nForce opaque when off",
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if vis(StatusPill::Isolate) {
|
if vis(StatusPill::Isolate) {
|
||||||
right_status = right_status.push(tip(
|
pills.push(
|
||||||
toggle_pill(crate::ui::icons::ST_ISOLATE, isolation_active, Message::ToggleIsolatePopup),
|
tip(
|
||||||
"Isolate Objects\nClick for Isolate / Hide / End",
|
toggle_pill(crate::ui::icons::ST_ISOLATE, isolation_active, Message::ToggleIsolatePopup),
|
||||||
));
|
"Isolate Objects\nClick for Isolate / Hide / End",
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if vis(StatusPill::QuickProps) {
|
if vis(StatusPill::QuickProps) {
|
||||||
right_status = right_status.push(tip(
|
pills.push(
|
||||||
toggle_pill(crate::ui::icons::ST_QUICKPROPS, quick_properties, Message::ToggleQuickProperties),
|
tip(
|
||||||
"Quick Properties\nFloating panel on selection",
|
toggle_pill(crate::ui::icons::ST_QUICKPROPS, quick_properties, Message::ToggleQuickProperties),
|
||||||
));
|
"Quick Properties\nFloating panel on selection",
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if vis(StatusPill::SelFilter) {
|
if vis(StatusPill::SelFilter) {
|
||||||
right_status = right_status.push(tip(
|
pills.push(
|
||||||
toggle_pill(
|
tip(
|
||||||
crate::ui::icons::ST_FILTER,
|
toggle_pill(
|
||||||
selection_filter_active,
|
crate::ui::icons::ST_FILTER,
|
||||||
Message::ToggleSelectionFilterPopup,
|
selection_filter_active,
|
||||||
),
|
Message::ToggleSelectionFilterPopup,
|
||||||
"Selection Filtering\nLimit which object types can be picked",
|
),
|
||||||
));
|
"Selection Filtering\nLimit which object types can be picked",
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if vis(StatusPill::SelCycle) {
|
if vis(StatusPill::SelCycle) {
|
||||||
right_status = right_status.push(tip(
|
pills.push(
|
||||||
toggle_pill(crate::ui::icons::ST_SELCYCLE, selection_cycling, Message::ToggleSelectionCycling),
|
tip(
|
||||||
"Selection Cycling\nRepeat-click to step through overlapping objects",
|
toggle_pill(crate::ui::icons::ST_SELCYCLE, selection_cycling, Message::ToggleSelectionCycling),
|
||||||
));
|
"Selection Cycling\nRepeat-click to step through overlapping objects",
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if vis(StatusPill::Vp) && !vp_label.is_empty() {
|
if vis(StatusPill::Vp) && !vp_label.is_empty() {
|
||||||
right_status = right_status.push(tip(
|
pills.push(
|
||||||
status_pill(vp_label).into(),
|
tip(
|
||||||
"Viewport count in active layout",
|
status_pill(vp_label).into(),
|
||||||
));
|
"Viewport count in active layout",
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if vis(StatusPill::CleanScreen) {
|
if vis(StatusPill::CleanScreen) {
|
||||||
right_status = right_status.push(tip(
|
pills.push(
|
||||||
toggle_pill(crate::ui::icons::ST_CLEANSCREEN, clean_screen, Message::ToggleCleanScreen),
|
tip(
|
||||||
"Clean Screen\nHide ribbon and panels",
|
toggle_pill(crate::ui::icons::ST_CLEANSCREEN, clean_screen, Message::ToggleCleanScreen),
|
||||||
));
|
"Clean Screen\nHide ribbon and panels",
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Customization handle: opens the pill show/hide menu.
|
// Customization handle: opens the pill show/hide menu.
|
||||||
right_status = right_status.push(tip(
|
pills.push(
|
||||||
customize_btn(),
|
tip(
|
||||||
"Customization\nShow or hide status-bar items",
|
customize_btn(),
|
||||||
));
|
"Customization\nShow or hide status-bar items",
|
||||||
let right_status = right_status;
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
let right_status = WrapFlow::new(pills).spacing_x(2.0).row_h(30.0);
|
||||||
|
|
||||||
let mut bar = Row::new().align_y(iced::Center).spacing(0);
|
// Left area: hamburger menu + Model/layout tabs in a flex-wrap flow, so
|
||||||
bar = bar.push(menu_btn);
|
// they spill onto lower rows when narrow (no scroll arrows). The pills
|
||||||
|
// wrap in their own flow; WrapBar stacks the two areas so a wrapped tab
|
||||||
|
// never shares a row with a pill.
|
||||||
|
let mut left: Vec<Element<'_, Message>> = Vec::new();
|
||||||
|
left.push(menu_btn.into());
|
||||||
if show_layout_tabs {
|
if show_layout_tabs {
|
||||||
// The layout tabs live in a horizontal scrollable that takes only
|
|
||||||
// the space left between the menu and the right-side tools, so many
|
|
||||||
// layouts scroll instead of pushing the tools off the bar (#199).
|
|
||||||
// ‹ › arrows scroll it (the mouse wheel works too).
|
|
||||||
let mut tabs_row = Row::new().align_y(iced::Center).spacing(0);
|
|
||||||
for name in layouts {
|
for name in layouts {
|
||||||
let is_active = name == current_layout;
|
let is_active = name == current_layout;
|
||||||
let renaming = rename_state
|
let renaming = rename_state
|
||||||
.filter(|(orig, _)| *orig == name)
|
.filter(|(orig, _)| *orig == name)
|
||||||
.map(|(_, edit)| edit.as_str());
|
.map(|(_, edit)| edit.as_str());
|
||||||
tabs_row = tabs_row.push(space_tab(name, is_active, renaming));
|
left.push(space_tab(name, is_active, renaming).into());
|
||||||
}
|
}
|
||||||
tabs_row = tabs_row.push(add_btn);
|
left.push(add_btn.into());
|
||||||
|
|
||||||
let tabs_scroll = scrollable(tabs_row)
|
|
||||||
.direction(Direction::Horizontal(Scrollbar::hidden()))
|
|
||||||
.id(iced::advanced::widget::Id::new(LAYOUT_TABS_SCROLL_ID))
|
|
||||||
.width(Length::Shrink);
|
|
||||||
|
|
||||||
bar = bar.push(scroll_arrow(false));
|
|
||||||
bar = bar.push(container(tabs_scroll).width(Length::Fill));
|
|
||||||
bar = bar.push(scroll_arrow(true));
|
|
||||||
} else {
|
|
||||||
bar = bar.push(iced::widget::Space::new().width(Length::Fill));
|
|
||||||
}
|
}
|
||||||
bar = bar.push(right_status);
|
let left_area = WrapFlow::new(left).spacing_x(2.0).row_h(30.0);
|
||||||
|
|
||||||
container(bar)
|
let wrap = WrapBar::new(left_area.into(), right_status.into())
|
||||||
|
.min_row_h(30.0)
|
||||||
|
.justify_end(true);
|
||||||
|
|
||||||
|
container(wrap)
|
||||||
.style(|_: &Theme| container::Style {
|
.style(|_: &Theme| container::Style {
|
||||||
background: Some(Background::Color(BAR_BG)),
|
background: Some(Background::Color(BAR_BG)),
|
||||||
border: Border {
|
border: Border {
|
||||||
|
|
@ -292,11 +329,10 @@ impl StatusBar {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
// Match the drawing (document) tab bar height so the three
|
// One row matches the drawing (document) tab bar height so the three
|
||||||
// horizontal strips — tabs, status bar, command line — line up.
|
// horizontal strips — tabs, status bar, command line — line up.
|
||||||
// `center_y` (not `height`) also vertically centres every pill in
|
// WrapBar vertically centres every pill (issue #216) and grows to a
|
||||||
// the taller bar instead of top-aligning them (issue #216).
|
// second row when the width can't hold both blocks.
|
||||||
.center_y(Length::Fixed(30.0))
|
|
||||||
.padding([0, 4])
|
.padding([0, 4])
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
@ -308,38 +344,6 @@ fn format_coords(p: glam::Vec3) -> String {
|
||||||
format!("{:.4}, {:.4}, {:.4}", p.x, p.y, p.z)
|
format!("{:.4}, {:.4}, {:.4}", p.x, p.y, p.z)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Layout-tab scroll arrow ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// A ‹ / › arrow that scrolls the layout-tab strip. `right` picks the
|
|
||||||
/// direction (and step sign).
|
|
||||||
fn scroll_arrow<'a>(right: bool) -> Element<'a, Message> {
|
|
||||||
let glyph = if right {
|
|
||||||
crate::ui::icons::arrow_right(12.0, ICON_COLOR)
|
|
||||||
} else {
|
|
||||||
crate::ui::icons::arrow_left(12.0, ICON_COLOR)
|
|
||||||
};
|
|
||||||
let step = if right {
|
|
||||||
LAYOUT_TAB_SCROLL_STEP
|
|
||||||
} else {
|
|
||||||
-LAYOUT_TAB_SCROLL_STEP
|
|
||||||
};
|
|
||||||
button(glyph)
|
|
||||||
.on_press(Message::ScrollLayoutTabs(step))
|
|
||||||
.style(|_: &Theme, status| button::Style {
|
|
||||||
background: Some(Background::Color(match status {
|
|
||||||
button::Status::Hovered => PILL_BG,
|
|
||||||
_ => Color::TRANSPARENT,
|
|
||||||
})),
|
|
||||||
border: Border {
|
|
||||||
radius: 3.0.into(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.padding([4, 6])
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Customization handle ──────────────────────────────────────────────────
|
// ── Customization handle ──────────────────────────────────────────────────
|
||||||
|
|
||||||
fn customize_btn() -> Element<'static, Message> {
|
fn customize_btn() -> Element<'static, Message> {
|
||||||
|
|
|
||||||
614
src/ui/wrap_bar.rs
Normal file
614
src/ui/wrap_bar.rs
Normal file
|
|
@ -0,0 +1,614 @@
|
||||||
|
//! Adaptive two-block bar that wraps its trailing block onto a second row when
|
||||||
|
//! the width can't hold both blocks side by side. Used by the ribbon tab row
|
||||||
|
//! (lead = quick-access + undo/redo, trail = tabs) and the status bar
|
||||||
|
//! (lead = menu, trail = status pills, with the layout-tab strip as the Fill
|
||||||
|
//! `middle` filler between them).
|
||||||
|
//!
|
||||||
|
//! Layout, per row:
|
||||||
|
//! • one row → `lead` at the left; `trail` packed after it (`justify_end`
|
||||||
|
//! false) or flush to the right edge (`justify_end` true);
|
||||||
|
//! an optional Fill `middle` stretches through the gap.
|
||||||
|
//! • two rows → `lead` (+ `middle` filling the rest) on row 1; `trail` on
|
||||||
|
//! row 2, left- or right-aligned per `justify_end`.
|
||||||
|
//!
|
||||||
|
//! The measured total height is written to `height_out` (if set) so callers can
|
||||||
|
//! anchor overlays below the possibly-taller bar.
|
||||||
|
|
||||||
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
pub struct WrapBar<'a> {
|
||||||
|
lead: Element<'a, Message>,
|
||||||
|
/// Optional Fill filler occupying the gap between `lead` and `trail`.
|
||||||
|
middle: Option<Element<'a, Message>>,
|
||||||
|
trail: Element<'a, Message>,
|
||||||
|
/// Gap between adjacent blocks on a shared row.
|
||||||
|
spacing: f32,
|
||||||
|
/// Minimum height of a single row.
|
||||||
|
min_row_h: f32,
|
||||||
|
/// When true, `trail` sits at the right edge (justified); otherwise it is
|
||||||
|
/// packed immediately after `lead`.
|
||||||
|
justify_end: bool,
|
||||||
|
/// Receives the measured total height (bits of an `f32`).
|
||||||
|
height_out: Option<Arc<AtomicU32>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> WrapBar<'a> {
|
||||||
|
pub fn new(lead: Element<'a, Message>, trail: Element<'a, Message>) -> Self {
|
||||||
|
Self {
|
||||||
|
lead,
|
||||||
|
middle: None,
|
||||||
|
trail,
|
||||||
|
spacing: 0.0,
|
||||||
|
min_row_h: 28.0,
|
||||||
|
justify_end: false,
|
||||||
|
height_out: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spacing(mut self, spacing: f32) -> Self {
|
||||||
|
self.spacing = spacing;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn min_row_h(mut self, h: f32) -> Self {
|
||||||
|
self.min_row_h = h;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn justify_end(mut self, justify: bool) -> Self {
|
||||||
|
self.justify_end = justify;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn middle(mut self, middle: Element<'a, Message>) -> Self {
|
||||||
|
self.middle = Some(middle);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn report_height(mut self, out: Arc<AtomicU32>) -> Self {
|
||||||
|
self.height_out = Some(out);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Elements in row order: lead, [middle], trail.
|
||||||
|
fn refs(&self) -> Vec<&Element<'a, Message>> {
|
||||||
|
let mut v = Vec::with_capacity(3);
|
||||||
|
v.push(&self.lead);
|
||||||
|
if let Some(m) = &self.middle {
|
||||||
|
v.push(m);
|
||||||
|
}
|
||||||
|
v.push(&self.trail);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refs_mut(&mut self) -> Vec<&mut Element<'a, Message>> {
|
||||||
|
let mut v = Vec::with_capacity(3);
|
||||||
|
v.push(&mut self.lead);
|
||||||
|
if let Some(m) = &mut self.middle {
|
||||||
|
v.push(m);
|
||||||
|
}
|
||||||
|
v.push(&mut self.trail);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget<Message, Theme, Renderer> for WrapBar<'a> {
|
||||||
|
fn children(&self) -> Vec<widget::Tree> {
|
||||||
|
self.refs().iter().map(|e| widget::Tree::new(*e)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&self, tree: &mut widget::Tree) {
|
||||||
|
let refs: Vec<_> = self.refs().iter().map(|e| e.as_widget()).collect();
|
||||||
|
tree.diff_children(&refs);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size::new(Length::Fill, Length::Shrink)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut widget::Tree,
|
||||||
|
renderer: &Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
let max = limits.max();
|
||||||
|
// Measure blocks at their natural (unwrapped) width so the fit decision
|
||||||
|
// and one-row placement use true content widths. A flex-wrap `trail`
|
||||||
|
// (WrapFlow) only wraps when it is later laid out with a bounded width.
|
||||||
|
let natural =
|
||||||
|
layout::Limits::new(Size::ZERO, Size::new(f32::INFINITY, f32::INFINITY));
|
||||||
|
|
||||||
|
let has_middle = self.middle.is_some();
|
||||||
|
let trail_idx = if has_middle { 2 } else { 1 };
|
||||||
|
|
||||||
|
let mut lead_node =
|
||||||
|
self.lead
|
||||||
|
.as_widget_mut()
|
||||||
|
.layout(&mut tree.children[0], renderer, &natural);
|
||||||
|
let mut trail_node = self.trail.as_widget_mut().layout(
|
||||||
|
&mut tree.children[trail_idx],
|
||||||
|
renderer,
|
||||||
|
&natural,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut lead_sz = lead_node.size();
|
||||||
|
let mut trail_sz = trail_node.size();
|
||||||
|
|
||||||
|
let width = if max.width.is_finite() {
|
||||||
|
max.width
|
||||||
|
} else {
|
||||||
|
lead_sz.width + self.spacing + trail_sz.width
|
||||||
|
};
|
||||||
|
|
||||||
|
let fits = max.width.is_finite()
|
||||||
|
&& lead_sz.width + self.spacing + trail_sz.width <= max.width;
|
||||||
|
|
||||||
|
let row_h = lead_sz.height.max(trail_sz.height).max(self.min_row_h);
|
||||||
|
let bounded = layout::Limits::new(Size::ZERO, Size::new(width, f32::INFINITY));
|
||||||
|
|
||||||
|
let (lead_pos, trail_pos, middle_x, middle_gap, total_h);
|
||||||
|
if fits {
|
||||||
|
// One row: lead left, trail packed after it or flush right.
|
||||||
|
let trail_x = if self.justify_end {
|
||||||
|
(width - trail_sz.width).max(lead_sz.width + self.spacing)
|
||||||
|
} else {
|
||||||
|
lead_sz.width + self.spacing
|
||||||
|
};
|
||||||
|
lead_pos = Point::new(0.0, (row_h - lead_sz.height) / 2.0);
|
||||||
|
trail_pos = Point::new(trail_x, (row_h - trail_sz.height) / 2.0);
|
||||||
|
middle_x = lead_sz.width + self.spacing;
|
||||||
|
middle_gap = (trail_x - self.spacing - middle_x).max(0.0);
|
||||||
|
total_h = row_h;
|
||||||
|
} else if has_middle {
|
||||||
|
// 3-slot justified bar: lead on row 1, middle fills the rest of it,
|
||||||
|
// trail (flex-wrap) drops onto the row(s) below.
|
||||||
|
lead_pos = Point::new(0.0, (row_h - lead_sz.height) / 2.0);
|
||||||
|
middle_x = lead_sz.width + self.spacing;
|
||||||
|
middle_gap = (width - middle_x).max(0.0);
|
||||||
|
|
||||||
|
trail_node = self.trail.as_widget_mut().layout(
|
||||||
|
&mut tree.children[trail_idx],
|
||||||
|
renderer,
|
||||||
|
&bounded,
|
||||||
|
);
|
||||||
|
trail_sz = trail_node.size();
|
||||||
|
trail_pos = Point::new(0.0, row_h);
|
||||||
|
total_h = row_h + trail_sz.height;
|
||||||
|
} else {
|
||||||
|
// 2-slot dual-wrap: lead and trail each wrap within their OWN row
|
||||||
|
// band, stacked so a wrapped lead item never lands on a trail row.
|
||||||
|
lead_node =
|
||||||
|
self.lead
|
||||||
|
.as_widget_mut()
|
||||||
|
.layout(&mut tree.children[0], renderer, &bounded);
|
||||||
|
lead_sz = lead_node.size();
|
||||||
|
let lead_h = lead_sz.height.max(self.min_row_h);
|
||||||
|
|
||||||
|
trail_node = self.trail.as_widget_mut().layout(
|
||||||
|
&mut tree.children[trail_idx],
|
||||||
|
renderer,
|
||||||
|
&bounded,
|
||||||
|
);
|
||||||
|
trail_sz = trail_node.size();
|
||||||
|
let trail_h = trail_sz.height.max(self.min_row_h);
|
||||||
|
|
||||||
|
lead_pos = Point::new(0.0, (lead_h - lead_sz.height) / 2.0);
|
||||||
|
trail_pos = Point::new(0.0, lead_h + (trail_h - trail_sz.height) / 2.0);
|
||||||
|
middle_x = 0.0;
|
||||||
|
middle_gap = 0.0;
|
||||||
|
total_h = lead_h + trail_h;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut children: Vec<layout::Node> = Vec::with_capacity(3);
|
||||||
|
children.push(lead_node.move_to(lead_pos));
|
||||||
|
|
||||||
|
if has_middle {
|
||||||
|
let mid_limits =
|
||||||
|
layout::Limits::new(Size::new(middle_gap, 0.0), Size::new(middle_gap, row_h));
|
||||||
|
let mut mid_node = self.middle.as_mut().unwrap().as_widget_mut().layout(
|
||||||
|
&mut tree.children[1],
|
||||||
|
renderer,
|
||||||
|
&mid_limits,
|
||||||
|
);
|
||||||
|
let mid_y = ((row_h - mid_node.size().height) / 2.0).max(0.0);
|
||||||
|
mid_node = mid_node.move_to(Point::new(middle_x, mid_y));
|
||||||
|
children.push(mid_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
children.push(trail_node.move_to(trail_pos));
|
||||||
|
|
||||||
|
if let Some(out) = &self.height_out {
|
||||||
|
out.store(total_h.to_bits(), Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
layout::Node::with_children(Size::new(width, total_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,
|
||||||
|
) {
|
||||||
|
for ((child, state), child_layout) in self
|
||||||
|
.refs_mut()
|
||||||
|
.into_iter()
|
||||||
|
.zip(tree.children.iter_mut())
|
||||||
|
.zip(layout.children())
|
||||||
|
{
|
||||||
|
child.as_widget_mut().update(
|
||||||
|
state,
|
||||||
|
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 mut interaction = mouse::Interaction::default();
|
||||||
|
for ((child, state), child_layout) in self
|
||||||
|
.refs()
|
||||||
|
.into_iter()
|
||||||
|
.zip(tree.children.iter())
|
||||||
|
.zip(layout.children())
|
||||||
|
{
|
||||||
|
let i = child.as_widget().mouse_interaction(
|
||||||
|
state,
|
||||||
|
child_layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
);
|
||||||
|
if i != mouse::Interaction::default() {
|
||||||
|
interaction = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interaction
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut widget::Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn widget::Operation,
|
||||||
|
) {
|
||||||
|
for ((child, state), child_layout) in self
|
||||||
|
.refs_mut()
|
||||||
|
.into_iter()
|
||||||
|
.zip(tree.children.iter_mut())
|
||||||
|
.zip(layout.children())
|
||||||
|
{
|
||||||
|
child
|
||||||
|
.as_widget_mut()
|
||||||
|
.operate(state, 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,
|
||||||
|
) {
|
||||||
|
for ((child, state), child_layout) in self
|
||||||
|
.refs()
|
||||||
|
.into_iter()
|
||||||
|
.zip(tree.children.iter())
|
||||||
|
.zip(layout.children())
|
||||||
|
{
|
||||||
|
child.as_widget().draw(
|
||||||
|
state,
|
||||||
|
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>> {
|
||||||
|
// At most one child (the hovered tooltip, if any) yields an overlay;
|
||||||
|
// return the first. Borrow fields and tree slots disjointly so the
|
||||||
|
// returned overlay can outlive this call.
|
||||||
|
let layouts: Vec<Layout<'b>> = layout.children().collect();
|
||||||
|
let has_middle = self.middle.is_some();
|
||||||
|
|
||||||
|
// Split the tree children into disjoint mutable slots.
|
||||||
|
let (lead_tree, rest) = tree.children.split_at_mut(1);
|
||||||
|
if let Some(ll) = layouts.first() {
|
||||||
|
if let Some(ov) = self.lead.as_widget_mut().overlay(
|
||||||
|
&mut lead_tree[0],
|
||||||
|
*ll,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
) {
|
||||||
|
return Some(ov);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if has_middle {
|
||||||
|
let (mid_tree, trail_tree) = rest.split_at_mut(1);
|
||||||
|
if let (Some(m), Some(ml)) = (self.middle.as_mut(), layouts.get(1)) {
|
||||||
|
if let Some(ov) = m.as_widget_mut().overlay(
|
||||||
|
&mut mid_tree[0],
|
||||||
|
*ml,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
) {
|
||||||
|
return Some(ov);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(tl) = layouts.get(2) {
|
||||||
|
return self.trail.as_widget_mut().overlay(
|
||||||
|
&mut trail_tree[0],
|
||||||
|
*tl,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if let Some(tl) = layouts.get(1) {
|
||||||
|
return self.trail.as_widget_mut().overlay(
|
||||||
|
&mut rest[0],
|
||||||
|
*tl,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<WrapBar<'a>> for Element<'a, Message> {
|
||||||
|
fn from(w: WrapBar<'a>) -> Self {
|
||||||
|
Element::new(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flex-wrap row: lays its items left-to-right and wraps onto a new row when
|
||||||
|
/// the next item would exceed the available width. Each row is `row_h` tall and
|
||||||
|
/// items are vertically centred. Used for the status-bar pills so they spread
|
||||||
|
/// across multiple rows when the width can't hold them on one line.
|
||||||
|
pub struct WrapFlow<'a> {
|
||||||
|
items: Vec<Element<'a, Message>>,
|
||||||
|
spacing_x: f32,
|
||||||
|
spacing_y: f32,
|
||||||
|
row_h: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> WrapFlow<'a> {
|
||||||
|
pub fn new(items: Vec<Element<'a, Message>>) -> Self {
|
||||||
|
Self {
|
||||||
|
items,
|
||||||
|
spacing_x: 2.0,
|
||||||
|
spacing_y: 0.0,
|
||||||
|
row_h: 28.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spacing_x(mut self, s: f32) -> Self {
|
||||||
|
self.spacing_x = s;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn row_h(mut self, h: f32) -> Self {
|
||||||
|
self.row_h = h;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget<Message, Theme, Renderer> for WrapFlow<'a> {
|
||||||
|
fn children(&self) -> Vec<widget::Tree> {
|
||||||
|
self.items.iter().map(widget::Tree::new).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&self, tree: &mut widget::Tree) {
|
||||||
|
let refs: Vec<_> = self.items.iter().map(|e| e.as_widget()).collect();
|
||||||
|
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 mut nodes: Vec<layout::Node> = Vec::with_capacity(self.items.len());
|
||||||
|
let mut x = 0.0f32;
|
||||||
|
let mut y = 0.0f32;
|
||||||
|
let mut used_w = 0.0f32;
|
||||||
|
|
||||||
|
for (item, state) in self.items.iter_mut().zip(tree.children.iter_mut()) {
|
||||||
|
let node = item.as_widget_mut().layout(state, renderer, &natural);
|
||||||
|
let sz = node.size();
|
||||||
|
if x > 0.0 && x + sz.width > max_w {
|
||||||
|
x = 0.0;
|
||||||
|
y += self.row_h + self.spacing_y;
|
||||||
|
}
|
||||||
|
let cy = y + ((self.row_h - sz.height) / 2.0).max(0.0);
|
||||||
|
nodes.push(node.move_to(Point::new(x, cy)));
|
||||||
|
x += sz.width + self.spacing_x;
|
||||||
|
used_w = used_w.max(x - self.spacing_x);
|
||||||
|
}
|
||||||
|
|
||||||
|
let total_h = if self.items.is_empty() {
|
||||||
|
self.row_h
|
||||||
|
} else {
|
||||||
|
y + self.row_h
|
||||||
|
};
|
||||||
|
layout::Node::with_children(Size::new(used_w.max(0.0), total_h), nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
) {
|
||||||
|
for ((item, state), child_layout) in self
|
||||||
|
.items
|
||||||
|
.iter_mut()
|
||||||
|
.zip(tree.children.iter_mut())
|
||||||
|
.zip(layout.children())
|
||||||
|
{
|
||||||
|
item.as_widget_mut().update(
|
||||||
|
state,
|
||||||
|
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 mut interaction = mouse::Interaction::default();
|
||||||
|
for ((item, state), child_layout) in self
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.zip(tree.children.iter())
|
||||||
|
.zip(layout.children())
|
||||||
|
{
|
||||||
|
let i = item.as_widget().mouse_interaction(
|
||||||
|
state,
|
||||||
|
child_layout,
|
||||||
|
cursor,
|
||||||
|
viewport,
|
||||||
|
renderer,
|
||||||
|
);
|
||||||
|
if i != mouse::Interaction::default() {
|
||||||
|
interaction = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interaction
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut widget::Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &Renderer,
|
||||||
|
operation: &mut dyn widget::Operation,
|
||||||
|
) {
|
||||||
|
for ((item, state), child_layout) in self
|
||||||
|
.items
|
||||||
|
.iter_mut()
|
||||||
|
.zip(tree.children.iter_mut())
|
||||||
|
.zip(layout.children())
|
||||||
|
{
|
||||||
|
item.as_widget_mut()
|
||||||
|
.operate(state, 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,
|
||||||
|
) {
|
||||||
|
for ((item, state), child_layout) in self
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.zip(tree.children.iter())
|
||||||
|
.zip(layout.children())
|
||||||
|
{
|
||||||
|
item.as_widget()
|
||||||
|
.draw(state, 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>> {
|
||||||
|
overlay::from_children(
|
||||||
|
&mut self.items,
|
||||||
|
tree,
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<WrapFlow<'a>> for Element<'a, Message> {
|
||||||
|
fn from(w: WrapFlow<'a>) -> Self {
|
||||||
|
Element::new(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue