refactor(ui): adopt iced layout widgets
This commit is contained in:
parent
c9c3d3b1bf
commit
5116369dec
6 changed files with 123 additions and 266 deletions
|
|
@ -52,7 +52,7 @@ iced = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00
|
|||
iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" }
|
||||
# iced_aw 0.15 compatibility is pending upstream in PR #432. Pin the exact
|
||||
# reviewed pair of commits so Iced and its additional widgets cannot drift.
|
||||
iced_aw = { git = "https://github.com/tsuza/iced_aw.git", rev = "a9301708a4d008dc17f63cbbbb2b2aa06e0e7352", features = ["menu", "context_menu", "color_picker"] }
|
||||
iced_aw = { git = "https://github.com/tsuza/iced_aw.git", rev = "a9301708a4d008dc17f63cbbbb2b2aa06e0e7352", features = ["menu", "context_menu", "color_picker", "drop_down"] }
|
||||
bytemuck = { version = "1.25", features = ["derive"] }
|
||||
glam = { version = "0.33", features = ["bytemuck"] }
|
||||
truck-modeling = "0.6"
|
||||
|
|
|
|||
|
|
@ -150,24 +150,20 @@ impl OpenCADStudio {
|
|||
// viewport draws the layout's own geometry (white sheet + entities +
|
||||
// borders) and the floating content viewports blit on top.
|
||||
let viewport_3d: Element<'_, Message> = if tab.is_start {
|
||||
responsive(|size| {
|
||||
start_page_view(
|
||||
&self.patrons,
|
||||
&self.videos,
|
||||
self.videos_loading,
|
||||
&self.video_thumbs,
|
||||
&self.discussions,
|
||||
self.discussions_loading,
|
||||
&self.recent_files,
|
||||
&self.recent_thumbs,
|
||||
self.recent_limit,
|
||||
&self.recent_limit_input,
|
||||
size.width,
|
||||
self.start_action_w.clone(),
|
||||
self.start_section,
|
||||
)
|
||||
})
|
||||
.into()
|
||||
start_page_view(
|
||||
&self.patrons,
|
||||
&self.videos,
|
||||
self.videos_loading,
|
||||
&self.video_thumbs,
|
||||
&self.discussions,
|
||||
self.discussions_loading,
|
||||
&self.recent_files,
|
||||
&self.recent_thumbs,
|
||||
self.recent_limit,
|
||||
&self.recent_limit_input,
|
||||
self.start_action_w.clone(),
|
||||
self.start_section,
|
||||
)
|
||||
} else if is_paper {
|
||||
shader(ViewportPane::model(
|
||||
&tab.scene,
|
||||
|
|
@ -2124,7 +2120,13 @@ pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Ele
|
|||
|
||||
items.push(new_btn.into());
|
||||
|
||||
container(WrapFlow::new(items).spacing_x(0.0).row_h(30.0))
|
||||
container(
|
||||
Row::with_children(items)
|
||||
.spacing(0.0)
|
||||
.align_y(iced::Center)
|
||||
.wrap()
|
||||
.vertical_spacing(0.0),
|
||||
)
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(
|
||||
theme.palette().background.base.color,
|
||||
|
|
@ -2254,6 +2256,43 @@ pub(super) fn collapse_bar<'a>(name: &str, on_press: Message) -> Element<'a, Mes
|
|||
}
|
||||
|
||||
pub(super) fn start_page_view<'a>(
|
||||
patrons: &'a [(String, i64)],
|
||||
videos: &'a [crate::videos::VideoEntry],
|
||||
videos_loading: bool,
|
||||
video_thumbs: &'a std::collections::HashMap<String, iced::widget::image::Handle>,
|
||||
discussions: &'a [crate::discussions::DiscussionEntry],
|
||||
discussions_loading: bool,
|
||||
recents: &'a [std::path::PathBuf],
|
||||
thumbs: &'a std::collections::HashMap<
|
||||
std::path::PathBuf,
|
||||
Option<iced::widget::image::Handle>,
|
||||
>,
|
||||
recent_limit: usize,
|
||||
recent_limit_input: &'a str,
|
||||
action_width_out: std::sync::Arc<std::sync::atomic::AtomicU32>,
|
||||
active: super::StartSection,
|
||||
) -> Element<'a, Message> {
|
||||
responsive(move |size| {
|
||||
start_page_content(
|
||||
patrons,
|
||||
videos,
|
||||
videos_loading,
|
||||
video_thumbs,
|
||||
discussions,
|
||||
discussions_loading,
|
||||
recents,
|
||||
thumbs,
|
||||
recent_limit,
|
||||
recent_limit_input,
|
||||
size.width,
|
||||
action_width_out.clone(),
|
||||
active,
|
||||
)
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
fn start_page_content<'a>(
|
||||
patrons: &'a [(String, i64)],
|
||||
videos: &'a [crate::videos::VideoEntry],
|
||||
videos_loading: bool,
|
||||
|
|
@ -2819,15 +2858,17 @@ pub(super) fn start_page_view<'a>(
|
|||
}
|
||||
})
|
||||
};
|
||||
let tab_bar = WrapFlow::new(vec![
|
||||
let tab_bar = Row::with_children(vec![
|
||||
tab_btn("Recent Files", super::StartSection::Recent).into(),
|
||||
tab_btn("Videos", super::StartSection::Videos).into(),
|
||||
tab_btn("Welcome", super::StartSection::Welcome).into(),
|
||||
tab_btn("Discussions", super::StartSection::Discussions).into(),
|
||||
tab_btn("Supporters", super::StartSection::Supporters).into(),
|
||||
])
|
||||
.spacing_x(6.0)
|
||||
.row_h(38.0);
|
||||
.spacing(6.0)
|
||||
.align_y(iced::Center)
|
||||
.wrap()
|
||||
.vertical_spacing(0.0);
|
||||
let section_body: Element<'a, Message> = match active {
|
||||
super::StartSection::Recent => container(recent)
|
||||
.width(Fill)
|
||||
|
|
|
|||
|
|
@ -6,11 +6,8 @@
|
|||
use crate::app::Message;
|
||||
use crate::ui::properties::acad_color_display;
|
||||
use acadrust::types::Color as AcadColor;
|
||||
use iced::advanced::layout::{self, Layout};
|
||||
use iced::advanced::widget::{self, Widget};
|
||||
use iced::advanced::{mouse, overlay, renderer, Shell};
|
||||
use iced::widget::{button, column, container, row, text};
|
||||
use iced::{Background, Border, Color, Element, Event, Length, Point, Rectangle, Renderer, Size, Theme, Vector};
|
||||
use iced::{Background, Border, Color, Element, Length, Theme};
|
||||
|
||||
/// Which "logical" entries the colour list offers besides the standard ACI
|
||||
/// colours.
|
||||
|
|
@ -127,6 +124,7 @@ pub fn color_selector<'a>(
|
|||
) -> Element<'a, Message> {
|
||||
let (cur_bg, _) = acad_color_display(current);
|
||||
let cur_name = color_display_name(current);
|
||||
let on_dismiss = on_toggle.clone();
|
||||
|
||||
// Closed button: current swatch + name + caret.
|
||||
let head = button(
|
||||
|
|
@ -162,12 +160,14 @@ pub fn color_selector<'a>(
|
|||
.padding(5)
|
||||
.width(220);
|
||||
|
||||
// The popup is shown as a floating overlay (anchored below the button) so
|
||||
// it doesn't push the surrounding form down.
|
||||
Element::new(Floating {
|
||||
base: head.into(),
|
||||
popup: popup.into(),
|
||||
})
|
||||
// `DropDown` keeps the popup outside the surrounding form layout and
|
||||
// handles viewport placement, Escape, and outside-click dismissal.
|
||||
iced_aw::DropDown::new(head, popup, true)
|
||||
.width(220)
|
||||
.alignment(iced_aw::drop_down::Alignment::Bottom)
|
||||
.offset(2.0)
|
||||
.on_dismiss(on_dismiss)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn list_row_style(theme: &Theme, status: button::Status) -> button::Style {
|
||||
|
|
@ -227,224 +227,19 @@ pub fn color_list<'a>(
|
|||
list.into()
|
||||
}
|
||||
|
||||
/// Render `base` inline with `popup` floating just below it — the shared
|
||||
/// dropdown mechanic for the panel's custom dropdowns (colour picker, block
|
||||
/// Name). Unlike iced's menu overlay it always opens downward.
|
||||
pub fn floating_below<'a>(
|
||||
/// Render `base` inline with `popup` in an `iced_aw` dropdown.
|
||||
pub fn drop_down_below<'a>(
|
||||
base: Element<'a, Message>,
|
||||
popup: Element<'a, Message>,
|
||||
popup_width: Length,
|
||||
popup_height: Length,
|
||||
on_dismiss: Message,
|
||||
) -> Element<'a, Message> {
|
||||
Element::new(Floating { base, popup })
|
||||
}
|
||||
|
||||
/// A widget that renders `base` inline and `popup` as a floating overlay
|
||||
/// anchored just below it.
|
||||
struct Floating<'a> {
|
||||
base: Element<'a, Message>,
|
||||
popup: Element<'a, Message>,
|
||||
}
|
||||
|
||||
impl<'a> Widget<Message, Theme, Renderer> for Floating<'a> {
|
||||
fn diff(&mut self, tree: &mut widget::Tree) {
|
||||
tree.diff_children(&mut [&mut self.base, &mut self.popup]);
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
self.base.as_widget().size()
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
self.base
|
||||
.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,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.base.as_widget_mut().update(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
layout,
|
||||
cursor,
|
||||
renderer,
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.base.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.base
|
||||
.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,
|
||||
) {
|
||||
self.base.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>> {
|
||||
let bounds = layout.bounds();
|
||||
let anchor = Point::new(
|
||||
bounds.x + translation.x,
|
||||
bounds.y + bounds.height + translation.y + 2.0,
|
||||
);
|
||||
Some(overlay::Element::new(Box::new(FloatingOverlay {
|
||||
popup: &mut self.popup,
|
||||
tree: &mut tree.children[1],
|
||||
anchor,
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Floating<'a>> for Element<'a, Message> {
|
||||
fn from(f: Floating<'a>) -> Self {
|
||||
Element::new(f)
|
||||
}
|
||||
}
|
||||
|
||||
struct FloatingOverlay<'a, 'b> {
|
||||
popup: &'b mut Element<'a, Message>,
|
||||
tree: &'b mut widget::Tree,
|
||||
anchor: Point,
|
||||
}
|
||||
|
||||
impl overlay::Overlay<Message, Theme, Renderer> for FloatingOverlay<'_, '_> {
|
||||
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
|
||||
.popup
|
||||
.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 {
|
||||
// Not enough room below — flip above the anchor.
|
||||
y = (self.anchor.y - bounds.height.min(0.0) - 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.popup.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,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) {
|
||||
let child = layout.children().next().unwrap();
|
||||
let vp = child.bounds();
|
||||
self.popup.as_widget_mut().update(
|
||||
self.tree, event, child, cursor, renderer, shell, &vp,
|
||||
);
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&mut self,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
let child = layout.children().next().unwrap();
|
||||
self.popup
|
||||
.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.popup
|
||||
.as_widget()
|
||||
.mouse_interaction(self.tree, child, cursor, &child.bounds(), renderer)
|
||||
}
|
||||
iced_aw::DropDown::new(base, popup, true)
|
||||
.width(popup_width)
|
||||
.height(popup_height)
|
||||
.alignment(iced_aw::drop_down::Alignment::Bottom)
|
||||
.offset(2.0)
|
||||
.on_dismiss(on_dismiss)
|
||||
.into()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ const COMBO_PAD_V: f32 = (ROW_H - FONT_SZ * 1.3 - 2.0) / 2.0; // fills combo to
|
|||
const SWATCH_SZ: f32 = ROW_H * 0.54; // ≈14 px color swatch
|
||||
const PATTERN_CARD_W: f32 = 158.0;
|
||||
const PATTERN_PREVIEW_H: f32 = 58.0;
|
||||
const PATTERN_PICKER_W: f32 = 348.0;
|
||||
const PATTERN_PICKER_H: f32 = 720.0;
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::scene::model::object::{PropSection, PropValue};
|
||||
|
|
@ -862,8 +864,7 @@ impl PropertiesPanel {
|
|||
/// Editable dropdown row (block reference Name): a text field with a caret
|
||||
/// button in one bordered control. Typing + Enter commits through the
|
||||
/// normal PropGeomCommit path (existing name → re-point, new name →
|
||||
/// rename); the caret opens a floating list of the definitions (always
|
||||
/// downward, via the shared `floating_below` mechanic) and picking one
|
||||
/// rename); the caret opens a dropdown list of the definitions and picking one
|
||||
/// applies through PropGeomChoiceChanged. Typed text filters the list.
|
||||
fn render_edit_choice_row<'a>(
|
||||
&'a self,
|
||||
|
|
@ -966,7 +967,13 @@ impl PropertiesPanel {
|
|||
|
||||
prop_row_widget(
|
||||
label,
|
||||
crate::ui::color_select::floating_below(head.into(), popup.into()),
|
||||
crate::ui::color_select::drop_down_below(
|
||||
head.into(),
|
||||
popup.into(),
|
||||
Length::Fixed(200.0),
|
||||
Length::Shrink,
|
||||
Message::PropEditChoiceToggle,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1124,19 +1131,25 @@ impl PropertiesPanel {
|
|||
.into()
|
||||
} else {
|
||||
scrollable(grid)
|
||||
.height(Length::Fixed(300.0))
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
};
|
||||
let popup = container(column![search, results].spacing(7))
|
||||
.style(container::bordered_box)
|
||||
.padding(8)
|
||||
.width(348)
|
||||
.height(Length::Fit.max(360.0));
|
||||
.width(PATTERN_PICKER_W)
|
||||
.height(Length::Fixed(PATTERN_PICKER_H));
|
||||
|
||||
prop_row_widget(
|
||||
label,
|
||||
crate::ui::color_select::floating_below(head.into(), popup.into()),
|
||||
crate::ui::color_select::drop_down_below(
|
||||
head.into(),
|
||||
popup.into(),
|
||||
Length::Fixed(PATTERN_PICKER_W),
|
||||
Length::Fixed(PATTERN_PICKER_H),
|
||||
Message::PropHatchPatternPickerToggle(current.to_string()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -393,7 +393,7 @@ impl Ribbon {
|
|||
redo_count: usize,
|
||||
) -> Element<'_, Message> {
|
||||
// ── Quick-access file commands + undo/redo, one merged flow ────────
|
||||
let lead = WrapFlow::new(vec![
|
||||
let lead = iced::widget::Row::with_children(vec![
|
||||
quick_access_btn(crate::ui::icons::DOC_NEW, "New", "NEW", is_start).into(),
|
||||
quick_access_btn(crate::ui::icons::FOLDER_OPEN, "Open", "OPEN", is_start).into(),
|
||||
quick_access_btn(crate::ui::icons::SAVE, "Save", "SAVE", is_start).into(),
|
||||
|
|
@ -402,8 +402,10 @@ impl Ribbon {
|
|||
render_history_control("Undo", UNDO_HISTORY_ID, undo_count, &self.open_dropdown).into(),
|
||||
render_history_control("Redo", REDO_HISTORY_ID, redo_count, &self.open_dropdown).into(),
|
||||
])
|
||||
.spacing_x(TOP_HIST_GAP)
|
||||
.row_h(28.0);
|
||||
.spacing(TOP_HIST_GAP)
|
||||
.align_y(iced::Center)
|
||||
.wrap()
|
||||
.vertical_spacing(0.0);
|
||||
|
||||
// The quick-access flow and the tabs flow each flex-wrap; WrapBar stacks
|
||||
// them so a wrapped tab never shares a row with a quick-access button.
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ use crate::app::Message;
|
|||
use crate::snap::Snapper;
|
||||
use crate::ui::statusbar::statusbar_config::{StatusBarConfig, StatusPill};
|
||||
use crate::ui::statusbar::status_menu::Entry as StatusMenuEntry;
|
||||
use crate::ui::wrap_bar::{WrapBar, WrapFlow};
|
||||
use crate::ui::wrap_bar::WrapBar;
|
||||
|
||||
pub struct StatusMenuData<'a> {
|
||||
pub layout_names: Vec<String>,
|
||||
|
|
@ -412,10 +412,12 @@ impl StatusBar {
|
|||
)
|
||||
.into(),
|
||||
);
|
||||
let right_status = WrapFlow::new(pills)
|
||||
.spacing_x(2.0)
|
||||
.row_h(30.0)
|
||||
.justify_end(true);
|
||||
let right_status = iced::widget::Row::with_children(pills)
|
||||
.spacing(2.0)
|
||||
.align_y(iced::Center)
|
||||
.wrap()
|
||||
.vertical_spacing(0.0)
|
||||
.align_x(iced::alignment::Horizontal::Right);
|
||||
|
||||
// Left area: hamburger menu + Model/layout tabs in a flex-wrap flow, so
|
||||
// they spill onto lower rows when narrow (no scroll arrows). The pills
|
||||
|
|
@ -462,7 +464,11 @@ impl StatusBar {
|
|||
}
|
||||
left.push(add_btn.into());
|
||||
}
|
||||
let left_area = WrapFlow::new(left).spacing_x(2.0).row_h(30.0);
|
||||
let left_area = iced::widget::Row::with_children(left)
|
||||
.spacing(2.0)
|
||||
.align_y(iced::Center)
|
||||
.wrap()
|
||||
.vertical_spacing(0.0);
|
||||
|
||||
let wrap = WrapBar::new(left_area.into(), right_status.into())
|
||||
.min_row_h(30.0)
|
||||
|
|
|
|||
Loading…
Reference in a new issue