file manager
git clone https://git.lucas.co/cce-files.git
refactor: widgets are handles the context owns
Page states take the context (BrowseState/NetworkState/SpaceState::new, browse::update, populate_graph); the breadcrumb helpers take a handle; PageContent's buttons are bare paint stamps. The prompt dialog's text box is inserted and removed with the dialog (set_prompt). This retires a heap corruption: the old build focused a stack-local TextBox by pointer and then moved it into its box, and opening a second prompt aborted with 'double free or corruption'. Shadow A/B identical on Browse, Graph, Space, the prompts and save mode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
src/main.rs | 227 ++++++++++++++++++++++++++-------------------------
src/pages/browse.rs | 167 +++++++++++++++++++++----------------
src/pages/mod.rs | 18 ++--
src/pages/network.rs | 85 ++++++++++---------
src/pages/space.rs | 22 +++--
5 files changed, 279 insertions(+), 240 deletions(-)
diff --git a/src/main.rs b/src/main.rs
index 1ac5aeb..b165d66 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,4 +1,4 @@
-use cce_ui::widget::Owned;
+use cce_ui::widget::Handle;
use cce_ui::cosmic_text::FontSystem;
use cce_ui::engine::{Application, LogicalPosition, LogicalSize, WindowSettings};
@@ -83,7 +83,7 @@ impl PromptKind {
struct PromptDialog {
kind: PromptKind,
path: std::path::PathBuf,
- textbox: Owned<cce_ui::widget::Adapted<cce_ui::widget::TextBox>>,
+ textbox: Handle<cce_ui::widget::Adapted<cce_ui::widget::TextBox>>,
}
fn prompt_rects(win_w: f32, win_h: f32) -> PromptRects {
@@ -600,11 +600,11 @@ struct FilesystemApp {
width: u32,
height: u32,
scale_factor: f64,
- page_buttons: Vec<(Owned<cce_ui::widget::Adapted<cce_ui::widget::Button>>, Message)>,
+ page_buttons: Vec<(cce_ui::widget::Adapted<cce_ui::widget::Button>, Message)>,
hovered_button: Option<usize>,
cursor_x: f32,
cursor_y: f32,
- paginator: Owned<cce_ui::widget::Adapted<Paginator>>,
+ paginator: Handle<cce_ui::widget::Adapted<Paginator>>,
just_initialized: bool,
ui_context: cce_ui::context::UiContext,
watcher: Option<notify::RecommendedWatcher>,
@@ -643,6 +643,14 @@ struct FilesystemApp {
// ── Layout Rebuild ──────────────────────────────────────────────────
impl FilesystemApp {
+ /// Show `dialog` in place of the prompt that is up, if any, whose text box
+ /// leaves the context with it.
+ fn set_prompt(&mut self, dialog: Option<PromptDialog>) {
+ if let Some(old) = std::mem::replace(&mut self.prompt_dialog, dialog) {
+ self.ui_context.remove(old.textbox);
+ }
+ }
+
/// Kick off a subtree scan if the Space page is showing a directory it has
/// not scanned. Cheap to call — it no-ops off the Space page, and while a
/// scan for the same directory is already running.
@@ -802,8 +810,8 @@ impl FilesystemApp {
};
self.ui_context.clear_hierarchy();
- self.browse.save_name_box.prepare_text(&mut self.font_system);
- self.browse.search_box.prepare_text(&mut self.font_system);
+ self.ui_context[self.browse.save_name_box].prepare_text(&mut self.font_system);
+ self.ui_context[self.browse.search_box].prepare_text(&mut self.font_system);
let mut widgets = Vec::new();
let mut texts = Vec::new();
@@ -816,21 +824,20 @@ impl FilesystemApp {
// child quads).
// Clear all widgets' hierarchy links
- self.paginator.clear_children(&mut self.ui_context); self.paginator.set_parent(None, &mut self.ui_context);
+ self.ui_context.lend_h(self.paginator, |w, ctx| { w.clear_children(ctx); w.set_parent(None, ctx); });
- self.browse.save_name_box.clear_children(&mut self.ui_context); self.browse.save_name_box.set_parent(None, &mut self.ui_context);
- self.browse.breadcrumb.clear_children(&mut self.ui_context); self.browse.breadcrumb.set_parent(None, &mut self.ui_context);
- self.network.breadcrumb.clear_children(&mut self.ui_context); self.network.breadcrumb.set_parent(None, &mut self.ui_context);
- self.network.graph.clear_children(&mut self.ui_context); self.network.graph.set_parent(None, &mut self.ui_context);
- self.space.breadcrumb.clear_children(&mut self.ui_context); self.space.breadcrumb.set_parent(None, &mut self.ui_context);
- if let Some(PromptDialog { textbox, .. }) = &mut self.prompt_dialog {
- textbox.clear_children(&mut self.ui_context);
- textbox.set_parent(None, &mut self.ui_context);
+ self.ui_context.lend_h(self.browse.save_name_box, |w, ctx| { w.clear_children(ctx); w.set_parent(None, ctx); });
+ self.ui_context.lend_h(self.browse.breadcrumb, |w, ctx| { w.clear_children(ctx); w.set_parent(None, ctx); });
+ self.ui_context.lend_h(self.network.breadcrumb, |w, ctx| { w.clear_children(ctx); w.set_parent(None, ctx); });
+ self.ui_context.lend_h(self.network.graph, |w, ctx| { w.clear_children(ctx); w.set_parent(None, ctx); });
+ self.ui_context.lend_h(self.space.breadcrumb, |w, ctx| { w.clear_children(ctx); w.set_parent(None, ctx); });
+ if let Some(tb) = self.prompt_dialog.as_ref().map(|d| d.textbox) {
+ self.ui_context.lend_h(tb, |w, ctx| { w.clear_children(ctx); w.set_parent(None, ctx); });
}
let has_sidebar = false;
- let sidebar_w = if has_sidebar { self.paginator.sidebar_w() } else { 0.0 };
+ let sidebar_w = if has_sidebar { self.ui_context[self.paginator].sidebar_w() } else { 0.0 };
// Inset from the WINDOW edge: the root plate's roll plus its padding
// (the padding alone left most of the run on the roll, so the edge
// read narrower than the gap between the panes).
@@ -846,14 +853,8 @@ impl FilesystemApp {
self.height as f32 - 2.0 * pad
};
- {
- let self_ptr = self as *mut Self;
- unsafe {
- if has_sidebar {
- self.ui_context.register_host(&mut self.paginator);
- (*self_ptr).paginator.set_parent(None, &mut self.ui_context);
- }
- }
+ if has_sidebar {
+ self.ui_context.lend_h(self.paginator, |w, ctx| w.set_parent(None, ctx));
}
// SplitBox + pane containers DISSOLVED (Phase 6y): the split is app state; the
@@ -875,17 +876,16 @@ impl FilesystemApp {
}
}
- if let Some(PromptDialog { textbox, .. }) = &mut self.prompt_dialog {
- self.ui_context.register_host(textbox);
- textbox.set_parent(None, &mut self.ui_context);
+ if let Some(tb) = self.prompt_dialog.as_ref().map(|d| d.textbox) {
+ self.ui_context.lend_h(tb, |w, ctx| w.set_parent(None, ctx));
}
// Layout widgets recursively inside the parent space
let mut dummy_pc = pages::PageContent::new();
if has_sidebar {
let page_idx = Page::ALL.iter().position(|&p| p == self.current_page).unwrap_or(0);
- self.paginator.set_selected_page(page_idx);
- cce_ui::layout::render_widget(&mut dummy_pc, &mut self.paginator, 0.0, 0.0, sidebar_w, self.height as f32, &mut self.ui_context);
+ self.ui_context[self.paginator].set_selected_page(page_idx);
+ cce_ui::layout::render_widget_h(&mut dummy_pc, self.paginator, 0.0, 0.0, sidebar_w, self.height as f32, &mut self.ui_context);
}
// Each top-level widget rendered through the same immediate-mode path the root
@@ -921,9 +921,9 @@ impl FilesystemApp {
(*self_ptr).preview.push_prims(&mut plain_pc);
}
}
- if let Some(PromptDialog { textbox, .. }) = &mut (*self_ptr).prompt_dialog {
- let (x, y, w, h) = textbox.rect();
- cce_ui::layout::render_widget(&mut plain_pc, textbox, x, y, w, h, &mut self.ui_context);
+ if let Some(tb) = self.prompt_dialog.as_ref().map(|d| d.textbox) {
+ let (x, y, w, h) = self.ui_context[tb].rect();
+ cce_ui::layout::render_widget_h(&mut plain_pc, tb, x, y, w, h, &mut self.ui_context);
}
// Legacy aggregate order: plain child quads, then rounded child quads.
@@ -1088,7 +1088,7 @@ impl FilesystemApp {
dialog_pc.text(kind.prompt(), dialog_x + dialog_pad, dialog_y + 42.0, 11.0, [0.54, 0.54, 0.58, 1.0]);
// Set textbox position dynamically using configured textbox height
- textbox.set_rect(tb_x, tb_y, tb_w, tb_h);
+ self.ui_context[*textbox].set_rect(tb_x, tb_y, tb_w, tb_h);
// Raised dialog plate + recessed command well (control_relief styling).
dialog_pc.relief_raised(dialog_x, dialog_y, dialog_w, dialog_h, 0.0);
dialog_pc.relief_recessed(tb_x, tb_y, tb_w, tb_h, cce_ui::layout::textbox_corner_radius());
@@ -1466,7 +1466,7 @@ impl Application for FilesystemApp {
}
// 3. If in the sidebar area (when sidebar is active), do not drag
if false {
- let sidebar_w = self.paginator.sidebar_w();
+ let sidebar_w = self.ui_context[self.paginator].sidebar_w();
if px <= sidebar_w {
return false;
}
@@ -1535,7 +1535,9 @@ impl Application for FilesystemApp {
cce_ui::scale::set_scale_factor(1.0);
- let browse = pages::browse::BrowseState::default();
+ // The context owns the widgets; the app keeps their handles.
+ let mut ui_context = cce_ui::context::UiContext::new();
+ let browse = pages::browse::BrowseState::new(&mut ui_context);
let _current_dir = browse.current_dir.clone();
let pages_names = Page::ALL.iter().map(|p| p.label().to_string()).collect::<Vec<_>>();
@@ -1546,8 +1548,8 @@ impl Application for FilesystemApp {
let app = Self {
current_page: Page::Browse,
browse,
- network: pages::network::NetworkState::default(),
- space: pages::space::SpaceState::default(),
+ network: pages::network::NetworkState::new(&mut ui_context),
+ space: pages::space::SpaceState::new(&mut ui_context),
preview: Default::default(),
preview_requested: None,
preview_generation: 0,
@@ -1571,9 +1573,9 @@ impl Application for FilesystemApp {
hovered_button: None,
cursor_x: 0.0,
cursor_y: 0.0,
- paginator: Owned::new(paginator),
+ paginator: ui_context.insert(paginator),
just_initialized: true,
- ui_context: cce_ui::context::UiContext::new(),
+ ui_context,
watcher: None,
fs_service,
seen_renderer: false,
@@ -1650,7 +1652,7 @@ impl Application for FilesystemApp {
Message::SwitchPage(page) => {
self.current_page = page;
let page_idx = Page::ALL.iter().position(|&p| p == page).unwrap_or(0);
- self.paginator.set_selected_page(page_idx);
+ self.ui_context[self.paginator].set_selected_page(page_idx);
// Switching to Space is what triggers the first scan — it is
// far too expensive to run for a page nobody is looking at.
self.ensure_space_scan();
@@ -1677,7 +1679,7 @@ impl Application for FilesystemApp {
_ => None,
};
- if let Some(req) = pages::browse::update(&mut self.browse, msg) {
+ if let Some(req) = pages::browse::update(&mut self.browse, msg, &mut self.ui_context) {
self.fs_service.send(req);
}
@@ -1699,17 +1701,17 @@ impl Application for FilesystemApp {
if let Some(idx) = self.browse.selected {
if let Some(entry) = self.browse.entries.get(idx) {
if self.select_mode {
- self.browse.save_name_box.text = entry.name.clone();
- if self.browse.save_name_box.editing {
- self.browse.save_name_box.edit_buffer = entry.name.clone();
+ self.ui_context[self.browse.save_name_box].text = entry.name.clone();
+ if self.ui_context[self.browse.save_name_box].editing {
+ self.ui_context[self.browse.save_name_box].edit_buffer = entry.name.clone();
}
}
}
} else {
if self.select_mode {
- self.browse.save_name_box.text.clear();
- if self.browse.save_name_box.editing {
- self.browse.save_name_box.edit_buffer.clear();
+ self.ui_context[self.browse.save_name_box].text.clear();
+ if self.ui_context[self.browse.save_name_box].editing {
+ self.ui_context[self.browse.save_name_box].edit_buffer.clear();
}
}
}
@@ -1745,10 +1747,10 @@ impl Application for FilesystemApp {
println!("{}", path.display());
std::process::exit(0);
} else {
- let filename = if self.browse.save_name_box.editing {
- self.browse.save_name_box.edit_buffer.trim()
+ let filename = if self.ui_context[self.browse.save_name_box].editing {
+ self.ui_context[self.browse.save_name_box].edit_buffer.trim()
} else {
- self.browse.save_name_box.text.trim()
+ self.ui_context[self.browse.save_name_box].text.trim()
};
if !filename.is_empty() {
let path = self.browse.current_dir.join(filename);
@@ -1797,8 +1799,9 @@ impl Application for FilesystemApp {
.with_max_width(None)
.with_placeholder("Program/Command");
tb.focus();
- self.ui_context.set_focused(&mut tb);
- self.prompt_dialog = Some(PromptDialog { kind: PromptKind::OpenWith, path, textbox: Owned::new(tb) });
+ let textbox = self.ui_context.insert(tb);
+ self.ui_context.set_focused_id(textbox.id());
+ self.set_prompt(Some(PromptDialog { kind: PromptKind::OpenWith, path, textbox }));
*needs_rebuild = true;
self.needs_rebuild = true;
}
@@ -1817,18 +1820,21 @@ impl Application for FilesystemApp {
tb.select_anchor = Some(0);
tb.all_selected = false;
tb.sync_editor_state();
- self.ui_context.set_focused(&mut tb);
- self.prompt_dialog = Some(PromptDialog { kind: PromptKind::Rename, path, textbox: Owned::new(tb) });
+ let textbox = self.ui_context.insert(tb);
+ self.ui_context.set_focused_id(textbox.id());
+ self.set_prompt(Some(PromptDialog { kind: PromptKind::Rename, path, textbox }));
*needs_rebuild = true;
self.needs_rebuild = true;
}
Message::PromptSubmit => {
if let Some(PromptDialog { kind, path, textbox }) = self.prompt_dialog.take() {
- let value = if textbox.editing {
- textbox.edit_buffer.trim().to_string()
- } else {
- textbox.text.trim().to_string()
- };
+ let value = self.ui_context.remove(textbox).map_or_else(String::new, |textbox| {
+ if textbox.editing {
+ textbox.edit_buffer.trim().to_string()
+ } else {
+ textbox.text.trim().to_string()
+ }
+ });
match kind {
PromptKind::OpenWith => {
if !value.is_empty() {
@@ -1848,7 +1854,7 @@ impl Application for FilesystemApp {
self.needs_rebuild = true;
}
Message::PromptCancel => {
- self.prompt_dialog = None;
+ self.set_prompt(None);
*needs_rebuild = true;
self.needs_rebuild = true;
}
@@ -1873,14 +1879,14 @@ impl Application for FilesystemApp {
if self.just_initialized {
self.just_initialized = false;
if self.save_mode {
- self.ui_context.focus_widget(&mut self.browse.save_name_box);
- self.ui_context.set_focused(&mut self.browse.save_name_box);
+ self.ui_context.focus_id(self.browse.save_name_box.id());
+ self.ui_context.set_focused_id(self.browse.save_name_box.id());
*needs_rebuild = true;
self.needs_rebuild = true;
}
}
- if self.paginator.tick(dt, &mut self.ui_context) {
+ if self.ui_context.lend_h(self.paginator, |w, ctx| w.tick(dt, ctx)).unwrap_or(false) {
*needs_rebuild = true;
self.needs_rebuild = true;
}
@@ -2166,7 +2172,7 @@ impl Application for FilesystemApp {
if !self.select_mode {
// Self-routing composite: handle_event, not propagate — the router's
// children-first descent would let the embedded strip consume this.
- if self.paginator.handle_event(&mv, &mut self.ui_context) {
+ if self.ui_context.lend_h(self.paginator, |w, ctx| w.handle_event(&mv, ctx)).unwrap_or(false) {
changed = true;
}
}
@@ -2324,7 +2330,7 @@ impl Application for FilesystemApp {
c.1 + dock::CORNER_R,
labels,
0,
- self.paginator.base().id(),
+ self.ui_context[self.paginator].base().id(),
);
self.plate_menu_actions = actions;
*needs_rebuild = true;
@@ -2354,7 +2360,7 @@ impl Application for FilesystemApp {
if state == ElementState::Pressed {
let clicked_inside = pos.x >= dialog_x && pos.x <= dialog_x + dialog_w && pos.y >= dialog_y && pos.y <= dialog_y + dialog_h;
if !clicked_inside {
- textbox.unfocus();
+ self.ui_context[*textbox].unfocus();
self.ui_context.clear_focus();
return Some(Message::PromptCancel);
}
@@ -2364,27 +2370,27 @@ impl Application for FilesystemApp {
let ev = cce_ui::widget::Event::MouseButton { button, state, x: pos.x, y: pos.y, local_x: pos.x, local_y: pos.y };
let root = textbox.id();
if self.ui_context.propagate_event(&ev, root) {
- self.ui_context.set_focused(textbox);
+ self.ui_context.set_focused_id(textbox.id());
*needs_rebuild = true;
self.needs_rebuild = true;
}
} else if pos.x >= btn_cancel_x && pos.x <= btn_cancel_x + btn_cancel_w && pos.y >= btn_cancel_y && pos.y <= btn_cancel_y + btn_cancel_h {
- textbox.unfocus();
+ self.ui_context[*textbox].unfocus();
self.ui_context.clear_focus();
return Some(Message::PromptCancel);
} else if pos.x >= btn_open_x && pos.x <= btn_open_x + btn_open_w && pos.y >= btn_open_y && pos.y <= btn_open_y + btn_open_h {
- textbox.unfocus();
+ self.ui_context[*textbox].unfocus();
self.ui_context.clear_focus();
return Some(Message::PromptSubmit);
} else {
- textbox.unfocus();
+ self.ui_context[*textbox].unfocus();
self.ui_context.clear_focus();
*needs_rebuild = true;
self.needs_rebuild = true;
}
}
} else {
- if button == MouseButton::Left && textbox.editing {
+ if button == MouseButton::Left && self.ui_context[*textbox].editing {
let ev = cce_ui::widget::Event::MouseButton { button, state, x: pos.x, y: pos.y, local_x: pos.x, local_y: pos.y };
let root = textbox.id();
if self.ui_context.propagate_event(&ev, root) {
@@ -2430,14 +2436,14 @@ impl Application for FilesystemApp {
}
if button == MouseButton::Right && state == ElementState::Pressed {
- let breadcrumb = match self.current_page {
- Page::Browse => &mut self.browse.breadcrumb,
- Page::Network => &mut self.network.breadcrumb,
- Page::Space => &mut self.space.breadcrumb,
+ let crumb = match self.current_page {
+ Page::Browse => self.browse.breadcrumb,
+ Page::Network => self.network.breadcrumb,
+ Page::Space => self.space.breadcrumb,
};
- if breadcrumb.hit_test(pos.x, pos.y, &self.ui_context) {
+ if self.ui_context[crumb].hit_test(pos.x, pos.y, &self.ui_context) {
let ev = cce_ui::widget::Event::MouseButton { button, state, x: pos.x, y: pos.y, local_x: pos.x, local_y: pos.y };
- let root = breadcrumb.id();
+ let root = crumb.id();
// The breadcrumb opens the toolkit's shared context menu itself
// (open_context_menu → segment header + Copy Path), recording
// the segment pressed. The app then shows that menu again,
@@ -2446,6 +2452,7 @@ impl Application for FilesystemApp {
// name and takes none from the app.
if self.ui_context.propagate_event(&ev, root) {
use cce_ui::widget::context_menu as cm;
+ let breadcrumb = &self.ui_context[crumb];
let seg = breadcrumb.right_clicked_seg.unwrap_or(breadcrumb.path.len());
let (labels, actions) = breadcrumb_menu_rows(breadcrumb.path_to_seg(seg), self.current_page);
let header = cm::options().into_iter().next().unwrap_or_default();
@@ -2531,7 +2538,7 @@ impl Application for FilesystemApp {
// run on under the breadcrumb and the preview.
let (x, y, w, h) = self.network.laid_out_for;
let inside = pos.x >= x && pos.x < x + w && pos.y >= y && pos.y < y + h;
- inside.then(|| self.network.graph.inner().node_at(pos.x, pos.y)).flatten()
+ inside.then(|| self.ui_context[self.network.graph].inner().node_at(pos.x, pos.y)).flatten()
})
.flatten()
.and_then(|idx| graph_node_menu(idx, &self.browse.current_dir, &self.browse.entries))
@@ -2570,10 +2577,10 @@ impl Application for FilesystemApp {
let ev = cce_ui::widget::Event::MouseButton { button, state, x: pos.x, y: pos.y, local_x: pos.x, local_y: pos.y };
let menu_match = !self.select_mode && {
// Self-routing composite: handle_event, not propagate (see pointer move).
- self.paginator.handle_event(&ev, &mut self.ui_context)
+ self.ui_context.lend_h(self.paginator, |w, ctx| w.handle_event(&ev, ctx)).unwrap_or(false)
};
if menu_match {
- if let Some((idx, _)) = self.paginator.menu_click() {
+ if let Some((idx, _)) = self.ui_context[self.paginator].menu_click() {
if idx < Page::ALL.len() {
self.ui_context.clear_focus();
self.current_page = Page::ALL[idx];
@@ -2650,7 +2657,7 @@ impl Application for FilesystemApp {
if self.select_mode {
if { let root = self.browse.save_name_box.id(); self.ui_context.propagate_event(&ev, root) } {
if state == ElementState::Pressed {
- self.ui_context.set_focused(&mut self.browse.save_name_box);
+ self.ui_context.set_focused_id(self.browse.save_name_box.id());
}
*needs_rebuild = true;
self.needs_rebuild = true;
@@ -2660,7 +2667,7 @@ impl Application for FilesystemApp {
&& button == MouseButton::Left
&& { let root = self.browse.search_box.id(); self.ui_context.propagate_event(&ev, root) }
{
- self.ui_context.set_focused(&mut self.browse.search_box);
+ self.ui_context.set_focused_id(self.browse.search_box.id());
*needs_rebuild = true;
self.needs_rebuild = true;
}
@@ -2677,9 +2684,9 @@ impl Application for FilesystemApp {
}
}
if button == MouseButton::Left && state == ElementState::Pressed {
- if self.browse.breadcrumb.hit_test(pos.x, pos.y, &self.ui_context) {
+ if self.ui_context[self.browse.breadcrumb].hit_test(pos.x, pos.y, &self.ui_context) {
if { let root = self.browse.breadcrumb.id(); self.ui_context.propagate_event(&ev, root) } {
- if let Some(seg) = self.browse.breadcrumb.path_click() {
+ if let Some(seg) = self.ui_context[self.browse.breadcrumb].path_click() {
let target_path = pages::browse::path_to_segment(&self.browse.current_dir, seg);
self.fs_service.send(services::fs::FsRequest::ReadDirectory(target_path));
*needs_rebuild = true;
@@ -2691,9 +2698,9 @@ impl Application for FilesystemApp {
} else if self.current_page == Page::Network {
if button == MouseButton::Left {
if state == ElementState::Pressed {
- if self.network.breadcrumb.hit_test(pos.x, pos.y, &self.ui_context) {
+ if self.ui_context[self.network.breadcrumb].hit_test(pos.x, pos.y, &self.ui_context) {
if { let root = self.network.breadcrumb.id(); self.ui_context.propagate_event(&ev, root) } {
- if let Some(seg) = self.network.breadcrumb.path_click() {
+ if let Some(seg) = self.ui_context[self.network.breadcrumb].path_click() {
let target_path = pages::browse::path_to_segment(&self.browse.current_dir, seg);
self.fs_service.send(services::fs::FsRequest::ReadDirectory(target_path));
changed = true;
@@ -2721,16 +2728,16 @@ impl Application for FilesystemApp {
// Sync selection from graph to browse state
let has_parent = self.browse.current_dir.parent().is_some();
let offset = if has_parent { 2 } else { 1 };
- if let Some(node_sel) = self.network.graph.selected_node() {
+ if let Some(node_sel) = self.ui_context[self.network.graph].selected_node() {
if node_sel >= offset {
let entry_idx = node_sel - offset;
if self.browse.selected != Some(entry_idx) {
self.browse.selected = Some(entry_idx);
if let Some(entry) = self.browse.entries.get(entry_idx) {
if self.select_mode {
- self.browse.save_name_box.text = entry.name.clone();
- if self.browse.save_name_box.editing {
- self.browse.save_name_box.edit_buffer = entry.name.clone();
+ self.ui_context[self.browse.save_name_box].text = entry.name.clone();
+ if self.ui_context[self.browse.save_name_box].editing {
+ self.ui_context[self.browse.save_name_box].edit_buffer = entry.name.clone();
}
}
let path = entry.path.clone();
@@ -2754,8 +2761,8 @@ impl Application for FilesystemApp {
}
// Sync double click navigation
- if let Some(dbl_idx) = self.network.graph.double_clicked_node() {
- self.network.graph.clear_double_clicked_node();
+ if let Some(dbl_idx) = self.ui_context[self.network.graph].double_clicked_node() {
+ self.ui_context[self.network.graph].clear_double_clicked_node();
if has_parent && dbl_idx == 0 {
if let Some(parent) = self.browse.current_dir.parent() {
let parent_path = parent.to_path_buf();
@@ -2770,9 +2777,9 @@ impl Application for FilesystemApp {
self.fs_service.send(services::fs::FsRequest::ReadDirectory(path));
changed = true;
} else if self.select_mode {
- self.browse.save_name_box.text = entry.name.clone();
- if self.browse.save_name_box.editing {
- self.browse.save_name_box.edit_buffer = entry.name.clone();
+ self.ui_context[self.browse.save_name_box].text = entry.name.clone();
+ if self.ui_context[self.browse.save_name_box].editing {
+ self.ui_context[self.browse.save_name_box].edit_buffer = entry.name.clone();
}
return Some(Message::SelectOpen);
}
@@ -2786,9 +2793,9 @@ impl Application for FilesystemApp {
}
} else if self.current_page == Page::Space {
if button == MouseButton::Left && state == ElementState::Pressed {
- if self.space.breadcrumb.hit_test(pos.x, pos.y, &self.ui_context) {
+ if self.ui_context[self.space.breadcrumb].hit_test(pos.x, pos.y, &self.ui_context) {
if { let root = self.space.breadcrumb.id(); self.ui_context.propagate_event(&ev, root) } {
- if let Some(seg) = self.space.breadcrumb.path_click() {
+ if let Some(seg) = self.ui_context[self.space.breadcrumb].path_click() {
let target_path = pages::browse::path_to_segment(&self.browse.current_dir, seg);
self.fs_service.send(services::fs::FsRequest::ReadDirectory(target_path));
changed = true;
@@ -2828,13 +2835,13 @@ impl Application for FilesystemApp {
}
if state == ElementState::Pressed {
- let clicked_search = self.current_page == Page::Browse && self.browse.search_visible && self.browse.search_box.hit_test(pos.x, pos.y, &self.ui_context);
- let clicked_save_name = self.select_mode && self.current_page == Page::Browse && self.browse.save_name_box.hit_test(pos.x, pos.y, &self.ui_context);
+ let clicked_search = self.current_page == Page::Browse && self.browse.search_visible && self.ui_context[self.browse.search_box].hit_test(pos.x, pos.y, &self.ui_context);
+ let clicked_save_name = self.select_mode && self.current_page == Page::Browse && self.ui_context[self.browse.save_name_box].hit_test(pos.x, pos.y, &self.ui_context);
if !clicked_search {
- self.ui_context.unfocus_widget(&mut self.browse.search_box);
+ self.ui_context.unfocus_id(self.browse.search_box.id());
}
if !clicked_save_name {
- self.ui_context.unfocus_widget(&mut self.browse.save_name_box);
+ self.ui_context.unfocus_id(self.browse.save_name_box.id());
}
if !clicked_search && !clicked_save_name {
self.ui_context.clear_focus();
@@ -2917,12 +2924,12 @@ impl Application for FilesystemApp {
if let Some(PromptDialog { textbox, .. }) = &mut self.prompt_dialog {
if event.logical_key == cce_ui::widget::Key::Named(cce_ui::widget::NamedKey::Enter) {
- textbox.unfocus();
+ self.ui_context[*textbox].unfocus();
self.ui_context.clear_focus();
return Some(Message::PromptSubmit);
}
if event.logical_key == cce_ui::widget::Key::Named(cce_ui::widget::NamedKey::Escape) {
- textbox.unfocus();
+ self.ui_context[*textbox].unfocus();
self.ui_context.clear_focus();
return Some(Message::PromptCancel);
}
@@ -2963,8 +2970,8 @@ impl Application for FilesystemApp {
let open_key = cce_ui::color::list_open_search_key();
if cce_ui::widget::match_key_shortcut(event, &open_key) {
self.browse.search_visible = true;
- self.ui_context.focus_widget(&mut self.browse.search_box);
- self.ui_context.set_focused(&mut self.browse.search_box);
+ self.ui_context.focus_id(self.browse.search_box.id());
+ self.ui_context.set_focused_id(self.browse.search_box.id());
*needs_rebuild = true;
self.needs_rebuild = true;
return None;
@@ -2973,7 +2980,7 @@ impl Application for FilesystemApp {
let close_key = cce_ui::color::list_close_search_key();
if cce_ui::widget::match_key_shortcut(event, &close_key) {
self.browse.search_visible = false;
- self.ui_context.unfocus_widget(&mut self.browse.search_box);
+ self.ui_context.unfocus_id(self.browse.search_box.id());
self.ui_context.clear_focus();
*needs_rebuild = true;
self.needs_rebuild = true;
@@ -2982,9 +2989,9 @@ impl Application for FilesystemApp {
if { let kev = cce_ui::widget::Event::KeyInput(event.clone()); let root = self.browse.search_box.id(); self.ui_context.propagate_event(&kev, root) } {
*needs_rebuild = true;
self.needs_rebuild = true;
- if self.browse.search_box.take_change() {
+ if self.ui_context[self.browse.search_box].take_change() {
return Some(Message::Browse(pages::browse::BrowseMessage::SearchChanged(
- self.browse.search_box.text.clone()
+ self.ui_context[self.browse.search_box].text.clone()
)));
}
return None;
@@ -2993,12 +3000,12 @@ impl Application for FilesystemApp {
}
// If the save_name_box is focused, forward key inputs to it
- if self.select_mode && self.browse.save_name_box.editing {
+ if self.select_mode && self.ui_context[self.browse.save_name_box].editing {
if { let kev = cce_ui::widget::Event::KeyInput(event.clone()); let root = self.browse.save_name_box.id(); self.ui_context.propagate_event(&kev, root) } {
*needs_rebuild = true;
self.needs_rebuild = true;
if event.logical_key == cce_ui::widget::Key::Named(cce_ui::widget::NamedKey::Enter) {
- self.ui_context.unfocus_widget(&mut self.browse.save_name_box);
+ self.ui_context.unfocus_id(self.browse.save_name_box.id());
self.ui_context.clear_focus();
return Some(Message::SelectOpen);
}
diff --git a/src/pages/browse.rs b/src/pages/browse.rs
index d21258b..d05a529 100644
--- a/src/pages/browse.rs
+++ b/src/pages/browse.rs
@@ -1,7 +1,8 @@
use std::path::{Path, PathBuf};
use crate::pages::PageContent;
-use cce_ui::widget::Owned;
+use cce_ui::context::UiContext;
+use cce_ui::widget::Handle;
use cce_ui::widget::{Adapted, WidgetHost, Breadcrumb, PathController};
use cce_ui::scene::arena::Arena;
use cce_ui::scene::layout::{compute_layout, CrossAlign, LayoutBox, Length, Size, Style};
@@ -21,7 +22,7 @@ pub struct DirEntry {
pub origin: Option<String>,
}
-#[derive(Debug, Clone)]
+#[derive(Debug)]
pub struct BrowseState {
pub current_dir: PathBuf,
pub all_entries: Vec<DirEntry>,
@@ -29,7 +30,7 @@ pub struct BrowseState {
pub show_hidden: bool,
pub list: crate::row_list::RowList,
pub search_visible: bool,
- pub search_box: Owned<cce_ui::widget::Adapted<cce_ui::widget::TextBox>>,
+ pub search_box: Handle<cce_ui::widget::Adapted<cce_ui::widget::TextBox>>,
pub selected: Option<usize>,
/// The selection the list was last auto-scrolled to. The layout pass runs
/// every frame, and an unconditional scroll_into_view there UNDID every
@@ -39,12 +40,13 @@ pub struct BrowseState {
pub autoscrolled_to: Option<usize>,
/// A just-created folder to select once a refresh lists it.
pub pending_select: Option<PathBuf>,
- pub breadcrumb: Owned<Adapted<Breadcrumb>>,
- pub save_name_box: Owned<cce_ui::widget::Adapted<cce_ui::widget::TextBox>>,
+ pub breadcrumb: Handle<Adapted<Breadcrumb>>,
+ pub save_name_box: Handle<cce_ui::widget::Adapted<cce_ui::widget::TextBox>>,
}
-impl Default for BrowseState {
- fn default() -> Self {
+impl BrowseState {
+ /// The page's state, its widgets inserted into `ctx`.
+ pub fn new(ctx: &mut UiContext) -> Self {
let home = std::env::var("HOME").unwrap_or_else(|_| "/".to_string());
let initial_dir = PathBuf::from(home);
let mut breadcrumb = Breadcrumb::new();
@@ -56,16 +58,16 @@ impl Default for BrowseState {
show_hidden: false,
list: crate::row_list::RowList::new(cce_ui::layout::button_height(), 2.0),
search_visible: false,
- search_box: Owned::new(cce_ui::widget::TextBox::new(String::new())
+ search_box: ctx.insert(cce_ui::widget::TextBox::new(String::new())
.with_placeholder("Search...")
.with_update_on_type(true)),
selected: None,
autoscrolled_to: None,
pending_select: None,
- breadcrumb: Owned::new(breadcrumb),
- save_name_box: Owned::new(cce_ui::widget::TextBox::new(String::new()).with_max_width(None)),
+ breadcrumb: ctx.insert(breadcrumb),
+ save_name_box: ctx.insert(cce_ui::widget::TextBox::new(String::new()).with_max_width(None)),
};
- state.update_breadcrumb();
+ state.update_breadcrumb(ctx);
state
}
}
@@ -76,7 +78,7 @@ impl BrowseState {
self.selected.and_then(|idx| self.entries.get(idx).map(|e| e.path.clone()))
}
- pub fn update_breadcrumb(&mut self) {
+ pub fn update_breadcrumb(&mut self, ctx: &mut UiContext) {
let mut segments = Vec::new();
for component in self.current_dir.components() {
let s = component.as_os_str().to_string_lossy().to_string();
@@ -84,7 +86,7 @@ impl BrowseState {
segments.push(s);
}
}
- self.breadcrumb.set_path(&segments);
+ ctx[self.breadcrumb].set_path(&segments);
}
}
@@ -248,7 +250,7 @@ pub fn view(state: &mut BrowseState, cx: f32, cy: f32, cw: f32, ch: f32, select_
// 1. The Breadcrumb, where every page puts it.
let (bx, by, bw, _) = at(crumb);
- crate::pages::breadcrumb_header(&mut pc, &mut state.breadcrumb, bx, by, bw, ctx);
+ crate::pages::breadcrumb_header(&mut pc, state.breadcrumb, bx, by, bw, ctx);
// 2. The list (ScrollBox).
let (list_x, list_y, list_w, list_h) = at(list);
@@ -372,7 +374,7 @@ pub fn view(state: &mut BrowseState, cx: f32, cy: f32, cw: f32, ch: f32, select_
list_w - 2.0 * search_pad,
search_h,
);
- cce_ui::layout::render_widget(&mut pc, &mut state.search_box, sx, sy, sw, sh, ctx);
+ cce_ui::layout::render_widget_h(&mut pc, state.search_box, sx, sy, sw, sh, ctx);
// The TextBox's recessed well lives in its modern paint(); the flat view
// this host renders through loses it, so carve it here.
pc.relief_recessed(sx, sy, sw, sh, cce_ui::layout::textbox_corner_radius());
@@ -394,8 +396,8 @@ pub fn view(state: &mut BrowseState, cx: f32, cy: f32, cw: f32, ch: f32, select_
// 3. Allocate and render Textbox(es)
if let Some(name_box) = name_box {
let (tx, ty, tw, th) = at(name_box);
- state.save_name_box.set_row_rect(tx, tw);
- cce_ui::layout::render_widget(&mut pc, &mut state.save_name_box, tx, ty, tw, th, ctx);
+ ctx[state.save_name_box].set_row_rect(tx, tw);
+ cce_ui::layout::render_widget_h(&mut pc, state.save_name_box, tx, ty, tw, th, ctx);
pc.relief_recessed(tx, ty, tw, th, cce_ui::layout::textbox_corner_radius());
}
@@ -404,11 +406,12 @@ pub fn view(state: &mut BrowseState, cx: f32, cy: f32, cw: f32, ch: f32, select_
// ── Update ──────────────────────────────────────────────────────────
-fn apply_filters(state: &mut BrowseState) {
- let search_query = if state.search_box.editing {
- state.search_box.edit_buffer.to_lowercase()
+fn apply_filters(state: &mut BrowseState, ctx: &UiContext) {
+ let search_box = &ctx[state.search_box];
+ let search_query = if search_box.editing {
+ search_box.edit_buffer.to_lowercase()
} else {
- state.search_box.text.to_lowercase()
+ search_box.text.to_lowercase()
};
state.entries = state
.all_entries
@@ -425,11 +428,11 @@ fn apply_filters(state: &mut BrowseState) {
state.selected = if state.entries.is_empty() { None } else { Some(0) };
}
-pub fn update(state: &mut BrowseState, msg: BrowseMessage) -> Option<crate::services::fs::FsRequest> {
+pub fn update(state: &mut BrowseState, msg: BrowseMessage, ctx: &mut UiContext) -> Option<crate::services::fs::FsRequest> {
match msg {
BrowseMessage::SearchChanged(q) => {
- state.search_box.text = q;
- apply_filters(state);
+ ctx[state.search_box].text = q;
+ apply_filters(state, ctx);
None
}
BrowseMessage::SelectEntry(i) => {
@@ -451,11 +454,11 @@ pub fn update(state: &mut BrowseState, msg: BrowseMessage) -> Option<crate::serv
state.current_dir = path.clone();
state.all_entries = entries;
state.pending_select = None;
- state.search_box.text.clear();
- state.search_box.edit_buffer.clear();
+ ctx[state.search_box].text.clear();
+ ctx[state.search_box].edit_buffer.clear();
state.search_visible = false;
- apply_filters(state);
- state.update_breadcrumb();
+ apply_filters(state, ctx);
+ state.update_breadcrumb(ctx);
Some(crate::services::fs::FsRequest::SaveLastDir(path))
}
BrowseMessage::DirectoryRefreshed(path, entries) => {
@@ -465,7 +468,7 @@ pub fn update(state: &mut BrowseState, msg: BrowseMessage) -> Option<crate::serv
.take()
.or_else(|| state.selected.and_then(|idx| state.entries.get(idx).map(|e| e.path.clone())));
state.all_entries = entries;
- apply_filters(state);
+ apply_filters(state, ctx);
if let Some(path) = selected_path {
state.selected = state.entries.iter().position(|e| e.path == path);
}
@@ -474,7 +477,7 @@ pub fn update(state: &mut BrowseState, msg: BrowseMessage) -> Option<crate::serv
}
BrowseMessage::ToggleHidden => {
state.show_hidden = !state.show_hidden;
- apply_filters(state);
+ apply_filters(state, ctx);
None
}
BrowseMessage::DeleteEntry(idx) => {
@@ -573,6 +576,7 @@ mod tests {
/// the kind decides which.
#[test]
fn entry_icon_names_a_glyph_by_kind() {
+ let mut ui = UiContext::new();
assert_eq!(entry_icon(true, "photos.png"), "folder");
assert_eq!(entry_icon(false, "Shot.PNG"), "file-image");
assert_eq!(entry_icon(false, "song.flac"), "file-audio");
@@ -587,7 +591,7 @@ mod tests {
}
}
- fn state_with_count(count: usize) -> BrowseState {
+ fn state_with_count(ui: &mut UiContext, count: usize) -> BrowseState {
BrowseState {
entries: (0..count)
.map(|i| DirEntry {
@@ -600,25 +604,28 @@ mod tests {
origin: None,
})
.collect(),
- ..BrowseState::default()
+ ..BrowseState::new(ui)
}
}
#[test]
fn down_from_none_selects_first_entry() {
- let state = state_with_count(3);
+ let mut ui = UiContext::new();
+ let state = state_with_count(&mut ui, 3);
assert_eq!(next_selection_index(&state, BrowseNavigation::Down), Some(0));
}
#[test]
fn up_from_none_selects_last_entry() {
- let state = state_with_count(3);
+ let mut ui = UiContext::new();
+ let state = state_with_count(&mut ui, 3);
assert_eq!(next_selection_index(&state, BrowseNavigation::Up), Some(2));
}
#[test]
fn navigation_stays_in_bounds() {
- let mut state = state_with_count(3);
+ let mut ui = UiContext::new();
+ let mut state = state_with_count(&mut ui, 3);
state.selected = Some(0);
assert_eq!(next_selection_index(&state, BrowseNavigation::Up), Some(0));
@@ -629,26 +636,28 @@ mod tests {
#[test]
fn apply_filters_hides_dotfiles() {
- let mut state = BrowseState::default();
+ let mut ui = UiContext::new();
+ let mut state = BrowseState::new(&mut ui);
state.all_entries = vec![
DirEntry { name: ".hidden".into(), path: PathBuf::from("/a/.hidden"), is_dir: false, size: 0, permissions: 0o644, modified: String::new(), origin: None },
DirEntry { name: "visible".into(), path: PathBuf::from("/a/visible"), is_dir: false, size: 0, permissions: 0o644, modified: String::new(), origin: None },
];
state.show_hidden = false;
- apply_filters(&mut state);
+ apply_filters(&mut state, &ui);
assert_eq!(state.entries.len(), 1);
assert_eq!(state.entries[0].name, "visible");
}
#[test]
fn apply_filters_shows_dotfiles_when_enabled() {
- let mut state = BrowseState::default();
+ let mut ui = UiContext::new();
+ let mut state = BrowseState::new(&mut ui);
state.all_entries = vec![
DirEntry { name: ".hidden".into(), path: PathBuf::from("/a/.hidden"), is_dir: false, size: 0, permissions: 0o644, modified: String::new(), origin: None },
DirEntry { name: "visible".into(), path: PathBuf::from("/a/visible"), is_dir: false, size: 0, permissions: 0o644, modified: String::new(), origin: None },
];
state.show_hidden = true;
- apply_filters(&mut state);
+ apply_filters(&mut state, &ui);
assert_eq!(state.entries.len(), 2);
}
@@ -666,9 +675,10 @@ mod tests {
#[test]
fn search_changed_filters_entries() {
- let mut state = BrowseState::default();
+ let mut ui = UiContext::new();
+ let mut state = BrowseState::new(&mut ui);
state.all_entries = vec![entry("apple", "/a/apple", false), entry("banana", "/a/banana", false)];
- let req = update(&mut state, BrowseMessage::SearchChanged("ban".to_string()));
+ let req = update(&mut state, BrowseMessage::SearchChanged("ban".to_string()), &mut ui);
assert!(req.is_none());
assert_eq!(state.entries.len(), 1);
assert_eq!(state.entries[0].name, "banana");
@@ -677,47 +687,52 @@ mod tests {
#[test]
fn navigate_to_path_reads_directory() {
- let mut state = BrowseState::default();
- let req = update(&mut state, BrowseMessage::NavigateToPath(PathBuf::from("/tmp")));
+ let mut ui = UiContext::new();
+ let mut state = BrowseState::new(&mut ui);
+ let req = update(&mut state, BrowseMessage::NavigateToPath(PathBuf::from("/tmp")), &mut ui);
assert!(matches!(req, Some(crate::services::fs::FsRequest::ReadDirectory(p)) if p == PathBuf::from("/tmp")));
}
#[test]
fn navigate_to_plain_directory_reads_it() {
+ let mut ui = UiContext::new();
let dir = std::env::temp_dir().join(format!("cce_nav_{}", chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)));
std::fs::create_dir_all(&dir).unwrap();
- let mut state = BrowseState::default();
+ let mut state = BrowseState::new(&mut ui);
state.entries = vec![entry("sub", dir.to_str().unwrap(), true)];
- let req = update(&mut state, BrowseMessage::NavigateTo(0));
+ let req = update(&mut state, BrowseMessage::NavigateTo(0), &mut ui);
assert!(matches!(req, Some(crate::services::fs::FsRequest::ReadDirectory(_))));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn navigate_to_project_directory_does_not_enter() {
+ let mut ui = UiContext::new();
let dir = std::env::temp_dir().join(format!("cce_proj_{}", chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("state.json"), "{}").unwrap();
- let mut state = BrowseState::default();
+ let mut state = BrowseState::new(&mut ui);
state.entries = vec![entry("proj", dir.to_str().unwrap(), true)];
- let req = update(&mut state, BrowseMessage::NavigateTo(0));
+ let req = update(&mut state, BrowseMessage::NavigateTo(0), &mut ui);
assert!(req.is_none());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn navigate_to_file_does_nothing() {
- let mut state = BrowseState::default();
+ let mut ui = UiContext::new();
+ let mut state = BrowseState::new(&mut ui);
state.entries = vec![entry("f.txt", "/a/f.txt", false)];
- let req = update(&mut state, BrowseMessage::NavigateTo(0));
+ let req = update(&mut state, BrowseMessage::NavigateTo(0), &mut ui);
assert!(req.is_none());
}
#[test]
fn directory_loaded_populates_and_saves() {
- let mut state = BrowseState::default();
+ let mut ui = UiContext::new();
+ let mut state = BrowseState::new(&mut ui);
let entries = vec![entry("x", "/d/x", false), entry("y", "/d/y", true)];
- let req = update(&mut state, BrowseMessage::DirectoryLoaded(PathBuf::from("/d"), entries));
+ let req = update(&mut state, BrowseMessage::DirectoryLoaded(PathBuf::from("/d"), entries), &mut ui);
assert_eq!(state.current_dir, PathBuf::from("/d"));
assert_eq!(state.entries.len(), 2);
assert!(matches!(req, Some(crate::services::fs::FsRequest::SaveLastDir(p)) if p == PathBuf::from("/d")));
@@ -725,13 +740,14 @@ mod tests {
#[test]
fn directory_refreshed_preserves_selection_by_path() {
- let mut state = BrowseState::default();
+ let mut ui = UiContext::new();
+ let mut state = BrowseState::new(&mut ui);
state.current_dir = PathBuf::from("/d");
state.all_entries = vec![entry("a", "/d/a", false), entry("b", "/d/b", false)];
- apply_filters(&mut state);
+ apply_filters(&mut state, &ui);
state.selected = Some(1); // "b"
let new_entries = vec![entry("b", "/d/b", false), entry("a", "/d/a", false), entry("c", "/d/c", false)];
- let req = update(&mut state, BrowseMessage::DirectoryRefreshed(PathBuf::from("/d"), new_entries));
+ let req = update(&mut state, BrowseMessage::DirectoryRefreshed(PathBuf::from("/d"), new_entries), &mut ui);
assert!(req.is_none());
assert_eq!(
state.selected.and_then(|i| state.entries.get(i)).map(|e| e.name.as_str()),
@@ -741,11 +757,12 @@ mod tests {
#[test]
fn directory_refreshed_ignores_other_dir() {
- let mut state = BrowseState::default();
+ let mut ui = UiContext::new();
+ let mut state = BrowseState::new(&mut ui);
state.current_dir = PathBuf::from("/d");
state.all_entries = vec![entry("a", "/d/a", false)];
- apply_filters(&mut state);
- let req = update(&mut state, BrowseMessage::DirectoryRefreshed(PathBuf::from("/other"), vec![entry("z", "/other/z", false)]));
+ apply_filters(&mut state, &ui);
+ let req = update(&mut state, BrowseMessage::DirectoryRefreshed(PathBuf::from("/other"), vec![entry("z", "/other/z", false)]), &mut ui);
assert!(req.is_none());
assert_eq!(state.entries.len(), 1);
assert_eq!(state.entries[0].name, "a");
@@ -753,11 +770,12 @@ mod tests {
#[test]
fn toggle_hidden_shows_dotfiles() {
- let mut state = BrowseState::default();
+ let mut ui = UiContext::new();
+ let mut state = BrowseState::new(&mut ui);
state.all_entries = vec![entry(".hidden", "/a/.hidden", false), entry("visible", "/a/visible", false)];
- apply_filters(&mut state);
+ apply_filters(&mut state, &ui);
assert_eq!(state.entries.len(), 1);
- let req = update(&mut state, BrowseMessage::ToggleHidden);
+ let req = update(&mut state, BrowseMessage::ToggleHidden, &mut ui);
assert!(req.is_none());
assert!(state.show_hidden);
assert_eq!(state.entries.len(), 2);
@@ -765,6 +783,7 @@ mod tests {
#[test]
fn navigate_enters_a_dir_holding_a_settings_state_kdl() {
+ let mut ui = UiContext::new();
// The shape of ~/.config/cce/cce-designer: a plain directory whose only content is
// the designer's own state.kdl. Treating that as a project made it un-enterable.
let dir = std::env::temp_dir().join(format!(
@@ -774,10 +793,10 @@ mod tests {
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("state.kdl"), "graph {\n grid_size_x (f64)71.0\n}").unwrap();
- let mut state = BrowseState::default();
+ let mut state = BrowseState::new(&mut ui);
state.all_entries = vec![entry("cce-designer", dir.to_str().unwrap(), true)];
- apply_filters(&mut state);
- let req = update(&mut state, BrowseMessage::NavigateTo(0));
+ apply_filters(&mut state, &ui);
+ let req = update(&mut state, BrowseMessage::NavigateTo(0), &mut ui);
assert!(
matches!(req, Some(crate::services::fs::FsRequest::ReadDirectory(ref p)) if *p == dir),
"navigating into it should read the directory, got {req:?}",
@@ -785,7 +804,7 @@ mod tests {
// A real project (state.json) still opens instead of being entered.
std::fs::write(dir.join("state.json"), "{}").unwrap();
- let req = update(&mut state, BrowseMessage::NavigateTo(0));
+ let req = update(&mut state, BrowseMessage::NavigateTo(0), &mut ui);
assert!(req.is_none(), "a project dir is not navigable");
let _ = std::fs::remove_file(dir.join("state.kdl"));
@@ -795,15 +814,17 @@ mod tests {
#[test]
fn last_dir_loaded_some_reads_that_dir() {
- let mut state = BrowseState::default();
- let req = update(&mut state, BrowseMessage::LastDirLoaded(Some(PathBuf::from("/some/dir"))));
+ let mut ui = UiContext::new();
+ let mut state = BrowseState::new(&mut ui);
+ let req = update(&mut state, BrowseMessage::LastDirLoaded(Some(PathBuf::from("/some/dir"))), &mut ui);
assert!(matches!(req, Some(crate::services::fs::FsRequest::ReadDirectory(p)) if p == PathBuf::from("/some/dir")));
}
#[test]
fn last_dir_loaded_none_falls_back() {
- let mut state = BrowseState::default();
- let req = update(&mut state, BrowseMessage::LastDirLoaded(None));
+ let mut ui = UiContext::new();
+ let mut state = BrowseState::new(&mut ui);
+ let req = update(&mut state, BrowseMessage::LastDirLoaded(None), &mut ui);
assert!(matches!(req, Some(crate::services::fs::FsRequest::ReadDirectory(_))));
}
@@ -843,6 +864,7 @@ mod tests {
#[test]
fn test_directory_persistence() {
+ let mut ui = UiContext::new();
let root = std::env::temp_dir().join(format!(
"cce_test_persist_{}",
chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)
@@ -860,6 +882,7 @@ mod tests {
#[tokio::test]
async fn test_delete_entry() {
+ let mut ui = UiContext::new();
let temp_dir = std::env::temp_dir();
let test_subdir = temp_dir.join(format!("cce_test_delete_{}", chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)));
std::fs::create_dir_all(&test_subdir).unwrap();
@@ -893,7 +916,7 @@ mod tests {
}
],
selected: Some(0),
- ..BrowseState::default()
+ ..BrowseState::new(&mut ui)
};
// Assert file exists before deletion
@@ -901,9 +924,9 @@ mod tests {
// DeleteEntry outside the trash routes to TrashPath (recoverable);
// DeleteEntryPermanent is the unrecoverable path.
- let req = update(&mut state, BrowseMessage::DeleteEntry(0));
+ let req = update(&mut state, BrowseMessage::DeleteEntry(0), &mut ui);
assert!(matches!(req, Some(crate::services::fs::FsRequest::TrashPath(_))));
- let req_perm = update(&mut state, BrowseMessage::DeleteEntryPermanent(0));
+ let req_perm = update(&mut state, BrowseMessage::DeleteEntryPermanent(0), &mut ui);
assert!(matches!(req_perm, Some(crate::services::fs::FsRequest::DeletePath(_, _))));
// Directly delete the file to simulate the FsService action
@@ -911,7 +934,7 @@ mod tests {
assert!(!file_path.exists());
// Perform update call for Deleted response
- let req2 = update(&mut state, BrowseMessage::Deleted(file_path.clone(), Ok(())));
+ let req2 = update(&mut state, BrowseMessage::Deleted(file_path.clone(), Ok(())), &mut ui);
assert!(matches!(req2, Some(crate::services::fs::FsRequest::ReadDirectory(_))));
// Check if state entries are updated
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
index 7c783bd..77fec03 100644
--- a/src/pages/mod.rs
+++ b/src/pages/mod.rs
@@ -3,7 +3,7 @@ pub mod preview;
pub mod network;
pub mod space;
-use cce_ui::widget::Owned;
+use cce_ui::widget::Handle;
use cce_ui::layout::RenderTarget;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -55,16 +55,16 @@ pub fn breadcrumb_h() -> f32 {
/// started their content 28px and 34px down against Browse's 24 + gap.
pub fn breadcrumb_header(
pc: &mut PageContent,
- breadcrumb: &mut Owned<cce_ui::widget::Adapted<cce_ui::widget::Breadcrumb>>,
+ breadcrumb: Handle<cce_ui::widget::Adapted<cce_ui::widget::Breadcrumb>>,
cx: f32,
cy: f32,
cw: f32,
ctx: &mut cce_ui::context::UiContext,
) -> f32 {
let h = breadcrumb_h();
- cce_ui::layout::render_widget(pc, breadcrumb, cx, cy, cw, h, ctx);
+ cce_ui::layout::render_widget_h(pc, breadcrumb, cx, cy, cw, h, ctx);
let rect = cce_ui::scene::layout::Rect { x: cx, y: cy, width: cw, height: h };
- breadcrumb_relief(pc, breadcrumb, rect);
+ breadcrumb_relief(pc, &ctx[breadcrumb], rect);
cy + h + cce_ui::layout::root_plate_gap()
}
@@ -101,7 +101,7 @@ pub fn breadcrumb_header(
/// this carve to `window_pc` would buy nothing.
pub fn breadcrumb_relief(
pc: &mut PageContent,
- breadcrumb: &Owned<cce_ui::widget::Adapted<cce_ui::widget::Breadcrumb>>,
+ breadcrumb: &cce_ui::widget::Adapted<cce_ui::widget::Breadcrumb>,
rect: cce_ui::scene::layout::Rect,
) {
let r = cce_ui::layout::dropdown_corner_radius();
@@ -173,7 +173,7 @@ impl Stroke {
pub struct PageContent {
pub rects: Vec<([f32; 4], f32, f32, f32, f32, f32, (bool, bool, bool, bool))>,
pub texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>, Option<[f32; 4]>)>,
- pub buttons: Vec<(Owned<cce_ui::widget::Adapted<cce_ui::widget::Button>>, crate::Message)>,
+ pub buttons: Vec<(cce_ui::widget::Adapted<cce_ui::widget::Button>, crate::Message)>,
/// Relief steps for the control_relief styling — (x, y, w, h, radius, depth,
/// kind: [`RELIEF_RECESSED`]/[`RELIEF_RAISED`]/[`RELIEF_INSET`]). The flat rects
/// own the faces; these are the edges-only walls emitted over them (the
@@ -423,7 +423,7 @@ impl PageContent {
.with_bg(bg)
.with_hover_bg(hover_bg)
.with_label_color(label_color);
- self.buttons.push((Owned::new(btn), action));
+ self.buttons.push((btn, action));
}
/// A button wearing the toolkit's own face — no per-call colors. The
@@ -438,7 +438,7 @@ impl PageContent {
.with_label(label)
.with_bg([0.0, 0.0, 0.0, 0.0])
.with_hover_bg([1.0, 1.0, 1.0, 0.10]);
- self.buttons.push((Owned::new(btn), action));
+ self.buttons.push((btn, action));
}
pub fn button_left(
@@ -459,7 +459,7 @@ impl PageContent {
.with_hover_bg(hover_bg)
.with_label_color(label_color)
.with_left_align(true);
- self.buttons.push((Owned::new(btn), action));
+ self.buttons.push((btn, action));
}
}
diff --git a/src/pages/network.rs b/src/pages/network.rs
index 4b9cfc8..e752a04 100644
--- a/src/pages/network.rs
+++ b/src/pages/network.rs
@@ -1,12 +1,13 @@
use std::path::{Path, PathBuf};
use crate::pages::PageContent;
use crate::pages::browse::{BrowseState, DirEntry};
-use cce_ui::widget::Owned;
+use cce_ui::context::UiContext;
+use cce_ui::widget::Handle;
use cce_ui::widget::{Adapted, Graph, GraphNode, Breadcrumb, GraphController, PathController};
pub struct NetworkState {
- pub graph: Owned<Adapted<Graph>>,
- pub breadcrumb: Owned<Adapted<Breadcrumb>>,
+ pub graph: Handle<Adapted<Graph>>,
+ pub breadcrumb: Handle<Adapted<Breadcrumb>>,
pub last_dir: PathBuf,
/// The graph pane `(x, y, w, h)` the nodes were last laid out for: a
/// pane of another size is laid out again (`view`), so the columns fit
@@ -20,8 +21,9 @@ pub struct NetworkState {
pub node_glyphs: Vec<&'static str>,
}
-impl Default for NetworkState {
- fn default() -> Self {
+impl NetworkState {
+ /// The page's state, its widgets inserted into `ctx`.
+ pub fn new(ctx: &mut UiContext) -> Self {
// The configured lattice and node body (`style.surface.graph`), as
// the designer's network uses them: a node is CENTRED on a lattice
// crossing and its name hangs off its right side. The pitch across is
@@ -42,8 +44,8 @@ impl Default for NetworkState {
breadcrumb.set_network_opacity(0.95);
Self {
- graph: Owned::new(graph),
- breadcrumb: Owned::new(breadcrumb),
+ graph: ctx.insert(graph),
+ breadcrumb: ctx.insert(breadcrumb),
last_dir: PathBuf::new(),
node_glyphs: Vec::new(),
laid_out_for: (0.0, 0.0, 0.0, 0.0),
@@ -107,21 +109,21 @@ impl NetworkState {
/// Lay the directory out for the pane `pane` and fill the graph — see
/// [`layout`]. The parent and the directory stand over the middle column,
/// its entries in rows below them.
- pub fn lay_out(&mut self, pane: (f32, f32, f32, f32), current_dir: &Path, entries: &[DirEntry]) {
- let node = self.graph.inner().node_size();
+ pub fn lay_out(&mut self, pane: (f32, f32, f32, f32), current_dir: &Path, entries: &[DirEntry], ctx: &mut UiContext) {
+ let node = ctx[self.graph].inner().node_size();
let (font, _) = label_metrics(node.0);
let names: Vec<String> = entries.iter().map(|e| fit_name(&e.name, font, NAME_MAX)).collect();
let ((pitch_x, pitch_y), cols, origin) = layout(pane, node, self.base_pitch, &names);
- self.graph.set_grid_pitch(pitch_x, pitch_y);
- self.graph.set_grid_origin(origin.0, origin.1);
- self.populate_graph(current_dir, entries, cols, &names);
+ ctx[self.graph].set_grid_pitch(pitch_x, pitch_y);
+ ctx[self.graph].set_grid_origin(origin.0, origin.1);
+ self.populate_graph(current_dir, entries, cols, &names, ctx);
self.laid_out_for = pane;
}
/// Fill the graph: the parent at row 1 and the directory at row 2 over
/// the middle column, then `entries` (shown as `names`) `cols` to a row,
/// columns counted from 1.
- pub fn populate_graph(&mut self, current_dir: &Path, entries: &[DirEntry], cols: usize, names: &[String]) {
+ pub fn populate_graph(&mut self, current_dir: &Path, entries: &[DirEntry], cols: usize, names: &[String], ctx: &mut UiContext) {
let cols = cols.max(1);
// Columns and rows from 1: the lattice's 0 lines are off the pane.
let mid = ((cols - 1) / 2) as f32 + 1.0;
@@ -196,7 +198,7 @@ impl NetworkState {
});
}
- self.graph.set_nodes(&nodes);
+ ctx[self.graph].set_nodes(&nodes);
self.node_glyphs = glyphs;
self.last_dir = current_dir.to_path_buf();
}
@@ -205,7 +207,7 @@ impl NetworkState {
pub fn view(state: &mut NetworkState, browse: &BrowseState, cx: f32, cy: f32, cw: f32, ch: f32, ctx: &mut cce_ui::context::UiContext) -> PageContent {
let mut pc = PageContent::new();
- let top = crate::pages::breadcrumb_header(&mut pc, &mut state.breadcrumb, cx, cy, cw, ctx);
+ let top = crate::pages::breadcrumb_header(&mut pc, state.breadcrumb, cx, cy, cw, ctx);
// The pane the graph is laid out in, under the breadcrumb.
let pane = (cx, top, cw, (cy + ch - top).max(0.0));
@@ -215,8 +217,8 @@ pub fn view(state: &mut NetworkState, browse: &BrowseState, cx: f32, cy: f32, cw
|| (pane.3 - state.laid_out_for.3).abs() > 0.5;
// Check if directory changed, or if last_dir is empty, and repopulate
- if state.last_dir != browse.current_dir || state.graph.get_nodes().is_empty() {
- state.lay_out(pane, &browse.current_dir, &browse.entries);
+ if state.last_dir != browse.current_dir || ctx[state.graph].get_nodes().is_empty() {
+ state.lay_out(pane, &browse.current_dir, &browse.entries, ctx);
// Update breadcrumb path
let mut segments = Vec::new();
@@ -226,22 +228,22 @@ pub fn view(state: &mut NetworkState, browse: &BrowseState, cx: f32, cy: f32, cw
segments.push(s);
}
}
- state.breadcrumb.set_path(&segments);
+ ctx[state.breadcrumb].set_path(&segments);
// Map browse selection to graph node selection if any
let has_parent = browse.current_dir.parent().is_some();
let offset = if has_parent { 2 } else { 1 };
if let Some(sel) = browse.selected {
- state.graph.set_selected_node(Some(sel + offset));
+ ctx[state.graph].set_selected_node(Some(sel + offset));
} else {
- state.graph.set_selected_node(None);
+ ctx[state.graph].set_selected_node(None);
}
} else {
// A pane of another size: the same nodes, laid out to fit it.
if moved {
- let selected = state.graph.selected_node();
- state.lay_out(pane, &browse.current_dir, &browse.entries);
- state.graph.set_selected_node(selected);
+ let selected = ctx[state.graph].selected_node();
+ state.lay_out(pane, &browse.current_dir, &browse.entries, ctx);
+ ctx[state.graph].set_selected_node(selected);
}
// Sync graph selection with browse selection when they are in sync
let has_parent = browse.current_dir.parent().is_some();
@@ -249,15 +251,15 @@ pub fn view(state: &mut NetworkState, browse: &BrowseState, cx: f32, cy: f32, cw
if let Some(sel) = browse.selected {
let expected_node_idx = sel + offset;
- if state.graph.selected_node() != Some(expected_node_idx) {
- if !state.graph.is_dragging() {
- state.graph.set_selected_node(Some(expected_node_idx));
+ if ctx[state.graph].selected_node() != Some(expected_node_idx) {
+ if !ctx[state.graph].is_dragging() {
+ ctx[state.graph].set_selected_node(Some(expected_node_idx));
}
}
} else {
- if let Some(graph_sel) = state.graph.selected_node() {
+ if let Some(graph_sel) = ctx[state.graph].selected_node() {
if graph_sel >= offset {
- state.graph.set_selected_node(None);
+ ctx[state.graph].set_selected_node(None);
}
}
}
@@ -267,7 +269,7 @@ pub fn view(state: &mut NetworkState, browse: &BrowseState, cx: f32, cy: f32, cw
// it, since a graph panned or zoomed past its edge would otherwise draw
// over the preview beside it.
let mut graph_pc = PageContent::new();
- cce_ui::layout::render_widget(&mut graph_pc, &mut state.graph, pane.0, pane.1, pane.2, pane.3, ctx);
+ cce_ui::layout::render_widget_h(&mut graph_pc, state.graph, pane.0, pane.1, pane.2, pane.3, ctx);
pc.absorb(graph_pc.clipped_to([cx, top, cx + cw, cy + ch]));
// Each node's glyph on the left of its body, at the size and inset the
@@ -276,7 +278,7 @@ pub fn view(state: &mut NetworkState, browse: &BrowseState, cx: f32, cy: f32, cw
let canvas = [cx, top, cx + cw, cy + ch];
let glyph_color = [0xcc as f32 / 255.0, 0xcc as f32 / 255.0, 0xd4 as f32 / 255.0, 1.0];
for (idx, glyph) in state.node_glyphs.iter().enumerate() {
- let Some((nx, ny, nw, nh)) = state.graph.node_rect(idx) else { continue };
+ let Some((nx, ny, nw, nh)) = ctx[state.graph].node_rect(idx) else { continue };
let scale_f = nw / 80.0;
let side = (18.0 * scale_f).clamp(6.0, 50.0);
pc.icon_bounded(glyph, nx + 6.0 * scale_f, ny + (nh - side) / 2.0, side, side, glyph_color, Some(canvas));
@@ -291,7 +293,8 @@ mod tests {
#[test]
fn test_populate_graph_has_parent() {
- let mut state = NetworkState::default();
+ let mut ui = UiContext::new();
+ let mut state = NetworkState::new(&mut ui);
let current_dir = Path::new("/home/user/project");
let entries = vec![
DirEntry {
@@ -314,8 +317,8 @@ mod tests {
},
];
- state.lay_out((0.0, 0.0, 800.0, 600.0), current_dir, &entries);
- let nodes = state.graph.get_nodes();
+ state.lay_out((0.0, 0.0, 800.0, 600.0), current_dir, &entries, &mut ui);
+ let nodes = ui[state.graph].get_nodes();
// 1 parent + 1 current + 2 children = 4 nodes
assert_eq!(nodes.len(), 4);
@@ -354,20 +357,21 @@ mod tests {
/// across its neighbour.
#[test]
fn the_graph_fits_its_pane_and_its_names_fit_their_columns() {
+ let mut ui = UiContext::new();
use cce_ui::widget::display::TextLabel;
- let mut state = NetworkState::default();
+ let mut state = NetworkState::new(&mut ui);
let names = ["Adwaita", "AdwaitaMono-BoldItalic.ttf", "a-really-quite-extraordinarily-long-font-family-name.otf", "x", "gnu-free", "noto-cjk", "liberation", "spleen", "xscreensaver"];
let entries: Vec<DirEntry> = names
.iter()
.map(|n| DirEntry { name: n.to_string(), path: PathBuf::from(format!("/f/{n}")), is_dir: false, size: 1, permissions: 0o644, modified: String::new(), origin: None })
.collect();
let pane = (20.0, 60.0, 600.0, 640.0);
- state.lay_out(pane, Path::new("/usr/share/fonts"), &entries);
- let g = state.graph.inner();
+ state.lay_out(pane, Path::new("/usr/share/fonts"), &entries, &mut ui);
+ let g = ui[state.graph].inner();
let (nw, _) = g.node_size();
let (font, gap) = label_metrics(nw);
let (pitch_x, _) = g.grid_pitch();
- let nodes = state.graph.get_nodes();
+ let nodes = ui[state.graph].get_nodes();
for (i, node) in nodes.iter().enumerate() {
let (x, _, w, _) = g.node_rect(i).unwrap();
let label_w = TextLabel::estimate_width(&node.name, font);
@@ -385,7 +389,8 @@ mod tests {
#[test]
fn test_populate_graph_root_no_parent() {
- let mut state = NetworkState::default();
+ let mut ui = UiContext::new();
+ let mut state = NetworkState::new(&mut ui);
let current_dir = Path::new("/");
let entries = vec![
DirEntry {
@@ -399,8 +404,8 @@ mod tests {
},
];
- state.lay_out((0.0, 0.0, 800.0, 600.0), current_dir, &entries);
- let nodes = state.graph.get_nodes();
+ state.lay_out((0.0, 0.0, 800.0, 600.0), current_dir, &entries, &mut ui);
+ let nodes = ui[state.graph].get_nodes();
// No parent, so 1 current + 1 child = 2 nodes
assert_eq!(nodes.len(), 2);
diff --git a/src/pages/space.rs b/src/pages/space.rs
index 12f47a6..5796c48 100644
--- a/src/pages/space.rs
+++ b/src/pages/space.rs
@@ -19,7 +19,8 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
-use cce_ui::widget::Owned;
+use cce_ui::context::UiContext;
+use cce_ui::widget::Handle;
use cce_ui::widget::{Adapted, Breadcrumb, PathController};
use crate::pages::PageContent;
@@ -406,7 +407,7 @@ pub fn update(state: &mut SpaceState, msg: SpaceMessage) {
// ── Page state ──────────────────────────────────────────────────────
pub struct SpaceState {
- pub breadcrumb: Owned<Adapted<Breadcrumb>>,
+ pub breadcrumb: Handle<Adapted<Breadcrumb>>,
/// The directory the current `tree` describes. Empty until a scan lands.
pub scanned_dir: PathBuf,
pub tree: Option<TreeNode>,
@@ -435,12 +436,13 @@ pub struct SpaceState {
pub focused: bool,
}
-impl Default for SpaceState {
- fn default() -> Self {
+impl SpaceState {
+ /// The page's state, its breadcrumb inserted into `ctx`.
+ pub fn new(ctx: &mut UiContext) -> Self {
let mut breadcrumb = Breadcrumb::new();
breadcrumb.set_network_opacity(0.95);
Self {
- breadcrumb: Owned::new(breadcrumb),
+ breadcrumb: ctx.insert(breadcrumb),
scanned_dir: PathBuf::new(),
tree: None,
tiles: Vec::new(),
@@ -676,7 +678,7 @@ pub fn view(
) -> PageContent {
let mut pc = PageContent::new();
- let top = crate::pages::breadcrumb_header(&mut pc, &mut state.breadcrumb, cx, cy, cw, ctx);
+ let top = crate::pages::breadcrumb_header(&mut pc, state.breadcrumb, cx, cy, cw, ctx);
let mut segments = Vec::new();
for component in browse.current_dir.components() {
@@ -685,7 +687,7 @@ pub fn view(
segments.push(s);
}
}
- state.breadcrumb.set_path(&segments);
+ ctx[state.breadcrumb].set_path(&segments);
// The map occupies everything below the header, less the footer readout.
let map = (cx, top, cw, (cy + ch - top - FOOTER_H).max(0.0));
@@ -1099,6 +1101,7 @@ mod tests {
#[test]
fn hit_test_finds_the_deepest_tile() {
+ let mut ui = UiContext::new();
let tree = TreeNode {
name: "root".into(),
size: 1000,
@@ -1110,7 +1113,7 @@ mod tests {
children: vec![file("leaf.bin", 1000)],
}],
};
- let mut state = SpaceState::default();
+ let mut state = SpaceState::new(&mut ui);
state.scanned_dir = PathBuf::from("/root");
state.tree = Some(tree);
state.relayout((0.0, 0.0, 400.0, 400.0));
@@ -1172,6 +1175,7 @@ mod tests {
#[test]
fn a_directory_drawn_as_one_block_wears_its_dominant_kind() {
+ let mut ui = UiContext::new();
// `sub` gets far less than MIN_RECURSE across, so it is not opened.
let mut children = vec![file("huge.bin", 1_000_000)];
children.push(TreeNode {
@@ -1181,7 +1185,7 @@ mod tests {
children: vec![file("x.mp3", 15_000), file("y.txt", 5_000)],
});
let tree = TreeNode { name: "root".into(), size: 1_020_000, is_dir: true, children };
- let mut state = SpaceState::default();
+ let mut state = SpaceState::new(&mut ui);
state.scan_finished(PathBuf::from("/root"), tree);
state.relayout((0.0, 0.0, 200.0, 200.0));