file manager
git clone https://git.lucas.co/cce-files.git
src/pages/mod.rs (20.1K)
1 pub mod browse;
2 pub mod preview;
3 pub mod network;
4 pub mod space;
5
6 use cce_ui::layout::RenderTarget;
7
8 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
9 pub enum Page {
10 Browse,
11 Network,
12 Space,
13 }
14
15 impl Page {
16 pub const ALL: [Page; 3] = [
17 Page::Browse,
18 Page::Network,
19 Page::Space,
20 ];
21
22 pub fn label(self) -> &'static str {
23 match self {
24 Page::Browse => "Browse",
25 Page::Network => "Network",
26 Page::Space => "Space",
27 }
28 }
29
30 pub fn icon(self) -> &'static str {
31 match self {
32 Page::Browse => "📁",
33 Page::Network => "🌐",
34 Page::Space => "▦",
35 }
36 }
37 }
38
39 /// Mirror the breadcrumb's relief into a flat-path [`PageContent`]: the
40 /// full-width recessed well, the ONE raised plate the segment run shares, and a
41 /// slanted seam engraved at each boundary between two segments.
42 ///
43 /// `render_widget` drops the relief prims `Breadcrumb::paint` emits, so every
44 /// page that shows a breadcrumb has to carve it app-side — this is that carve,
45 /// in one place, since all three pages want it identically.
46 ///
47 /// **Audited against `Breadcrumb::paint` and deliberately left page-side** —
48 /// it does NOT need [`dropdown_relief`]'s treatment, for two reasons that are
49 /// easy to assume away:
50 ///
51 /// - *The depths already agree.* Each `relief_*`/`groove` helper derives depth
52 /// from the height it is handed, and all three here are handed what the
53 /// widget uses: the well from `rect.height`, the plate from `rh`, and the
54 /// seams from the run as host — the widget engraves the seams at the RUN's
55 /// depth, not the well's. Nothing is pre-expanded, so nothing drifts the way
56 /// the dropdown's ring did.
57 /// - *The rect is fresh.* Every caller builds it as a literal on the line after
58 /// laying the breadcrumb out, rather than reading it back off the widget, so
59 /// there is no previous-frame rect to pick up.
60 ///
61 /// Nor does the `window_pc`-vs-`pc` split matter here, though `dropdown_relief`
62 /// warns loudly about it. `display_list` emits ALL of a `PageContent`'s rects
63 /// before ALL of its reliefs, so the call order within `pc` is irrelevant; only
64 /// a different PageContent could reorder these. The only quad the breadcrumb
65 /// puts under the carve is the hover tint, and that is inset to the seam's
66 /// furthest lean by construction, so it does not overlap the grooves. Moving
67 /// this carve to `window_pc` would buy nothing.
68 pub fn breadcrumb_relief(
69 pc: &mut PageContent,
70 breadcrumb: &cce_ui::widget::Adapted<cce_ui::widget::Breadcrumb>,
71 rect: cce_ui::scene::layout::Rect,
72 ) {
73 let r = cce_ui::layout::dropdown_corner_radius();
74 let Some(run) = breadcrumb.run_box(rect) else { return };
75 let (rx, ry, rw, rh) = run;
76 // The dropdown's flush inset plate on the segment run (mirroring
77 // Breadcrumb::paint's relief branch) — the full-rect recessed well and
78 // the raised run inside it are gone with the restyle.
79 //
80 // Face AND ring, through the `inset_plate` bridge, because the face is
81 // the dropdown's configured fill: `render_widget` offers that fill for a
82 // Dropdown through a per-type hook (layout.rs) but has no Breadcrumb arm,
83 // so a run carved here with no face would keep showing the window plate
84 // while the dropdown beside it went opaque. Both controls read
85 // `dropdown_background_color` now, so they match under any config —
86 // transparent leaves the plate as the face for both.
87 let radius = r.min(rh * 0.5);
88 let depth = cce_ui::layout::bevel_width().min(rh * 0.2);
89 let raw = cce_ui::color::dropdown_background_color();
90 let face = if raw[3] > 0.001 {
91 let mut c = raw;
92 c[3] = 1.0;
93 c
94 } else {
95 [0.0; 4]
96 };
97 pc.inset_plate(face, rx, ry, rw, rh, radius, depth);
98 for (a, b) in breadcrumb.seams(rect) {
99 pc.groove(a, b, cce_ui::widget::Breadcrumb::SEAM_WIDTH, run);
100 }
101 }
102
103 /// Mirror the view dropdown's flush inset plate into a flat-path
104 /// [`PageContent`]: the groove ring sunk around the control, and the control's
105 /// own edge rolling back up out of it — face level with the window plate, so
106 /// the seam is the only thing saying it is a separate part.
107 ///
108 /// `Dropdown::paint` emits this as one `ctx.inset_plate`; `render_widget` keeps
109 /// only quads and text, so the carve is app-side — the same story as
110 /// [`breadcrumb_relief`], which is the well-and-plate this pairs with.
111 ///
112 /// It reaches the SAME `inset_plate` call the widget makes, via
113 /// [`PageContent::relief_inset`] → `WidgetFx::Inset`. It used to hand-roll the
114 /// pair `inset_plate` expands to (`relief_recessed` over an expanded rect, then
115 /// `relief_raised`) — which got the ring's depth wrong, because
116 /// `relief_recessed` derives depth from the height it is HANDED, and that was
117 /// the already-expanded one: a 5.76px wall against a 4.8px lip, so the
118 /// descending wall over-ran the lip instead of meeting it in the tight V-groove
119 /// with no flat floor that `inset_plate` documents.
120 ///
121 /// **Carve it into `window_pc`, AFTER the page's `view()` has run.** Two
122 /// constraints pin it there, and they pull in opposite directions:
123 ///
124 /// - *After the pages* — because the pages are what lay the dropdown out. Read
125 /// `view_dropdown.rect()` before they run and you get the rect they assigned
126 /// on the PREVIOUS frame, so the ring trails the control by a frame through a
127 /// resize (and on the very first frame it carves a 0×0 rect).
128 /// - *Into `window_pc`, not the page's own `pc`* — because these are overlay
129 /// carves, shaded against whatever is already beneath them. Emitting them
130 /// page-side puts them after the dropdown's own background quad instead of
131 /// before it, which visibly thins the lit top rim. Same rect, different
132 /// material. (Verified by pixel-diffing the two orders; `CCE_PLATE_DEBUG=1`
133 /// shows both as overlay fallback, so this is compositing order, not
134 /// plate grouping.)
135 pub fn dropdown_relief(pc: &mut PageContent, rect: cce_ui::scene::layout::Rect) {
136 pc.relief_inset(
137 rect.x,
138 rect.y,
139 rect.width,
140 rect.height,
141 cce_ui::layout::dropdown_corner_radius(),
142 );
143 }
144
145 pub const RELIEF_RECESSED: u8 = 0;
146 pub const RELIEF_RAISED: u8 = 1;
147 pub const RELIEF_INSET: u8 = 2;
148 /// A recessed well with pointer focus: renders as the tinted carve — the
149 /// wrapped accent glint REPLACING the relief lighting (the DE's one focus
150 /// language, same treatment as a focused plate's ring).
151 pub const RELIEF_RECESSED_FOCUS: u8 = 3;
152 /// [`RELIEF_INSET`] with keyboard focus: the flush plate's rim lit in the
153 /// highlight — the ring a focused control plate wears (`ControlPlate::with_tint`).
154 pub const RELIEF_INSET_FOCUS: u8 = 4;
155
156 pub struct PageContent {
157 pub rects: Vec<([f32; 4], f32, f32, f32, f32, f32, (bool, bool, bool, bool))>,
158 pub texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>, Option<[f32; 4]>)>,
159 pub buttons: Vec<(cce_ui::widget::Adapted<cce_ui::widget::Button>, crate::Message)>,
160 /// Relief steps for the control_relief styling — (x, y, w, h, radius, depth,
161 /// kind: [`RELIEF_RECESSED`]/[`RELIEF_RAISED`]/[`RELIEF_INSET`]). The flat rects
162 /// own the faces; these are the edges-only walls emitted over them (the
163 /// ParametersBg::reliefs idiom for flat-view hosts).
164 pub reliefs: Vec<(f32, f32, f32, f32, f32, f32, u8)>,
165 /// Engraved lines over the flat rects — (ax, ay, bx, by, width, depth) plus
166 /// the (x, y, w, h) of the surface being engraved, which the shading fades
167 /// out against. Unlike [`reliefs`] these are not axis-aligned: this is the
168 /// breadcrumb's slanted segment seams.
169 ///
170 /// [`reliefs`]: PageContent::reliefs
171 pub grooves: Vec<(f32, f32, f32, f32, f32, f32, f32, f32, f32, f32)>,
172 /// GPU-textured quads — (image id from `cce_ui::vk::upload_rgba`, x, y, w, h,
173 /// alpha). Drawn after the part's rects, so a fill emitted earlier is the floor
174 /// beneath the image and overlay parts still cover it.
175 pub images: Vec<(u32, f32, f32, f32, f32, f32)>,
176 /// Lit plates — (color, x, y, w, h, radius, depth). Unlike [`reliefs`], a plate
177 /// owns its FILL as well as its edge: one primitive carrying a rounded face and
178 /// the rolled, lit perimeter, shaded in a single lighting evaluation. That is
179 /// what the pane plates and the preview stub wear, so a floating surface
180 /// (the context menu) reads as the same material rather than as a flat chip
181 /// inside a drawn frame.
182 ///
183 /// [`reliefs`]: PageContent::reliefs
184 pub plates: Vec<([f32; 4], f32, f32, f32, f32, f32, f32)>,
185 }
186
187 impl PageContent {
188 pub fn new() -> Self {
189 Self {
190 rects: Vec::new(),
191 texts: Vec::new(),
192 buttons: Vec::new(),
193 reliefs: Vec::new(),
194 grooves: Vec::new(),
195 images: Vec::new(),
196 plates: Vec::new(),
197 }
198 }
199
200 /// Move every part of `other` into this content.
201 ///
202 /// Field-complete by construction: this replaces four hand-listed runs of
203 /// `self.x.extend(other.x)` in `rebuild_layout`, which silently dropped any
204 /// vec nobody remembered to add a line for — `grooves` was invisible for
205 /// exactly that reason. The destructuring below turns a new field into a
206 /// compile error instead of a missing mark on screen.
207 pub fn absorb(&mut self, other: PageContent) {
208 let PageContent { rects, texts, buttons, reliefs, grooves, images, plates } = other;
209 self.rects.extend(rects);
210 self.texts.extend(texts);
211 self.buttons.extend(buttons);
212 self.reliefs.extend(reliefs);
213 self.grooves.extend(grooves);
214 self.images.extend(images);
215 self.plates.extend(plates);
216 }
217
218 /// A GPU-textured quad (id from `cce_ui::vk::upload_rgba`).
219 pub fn image(&mut self, id: u32, x: f32, y: f32, w: f32, h: f32, alpha: f32) {
220 self.images.push((id, x, y, w, h, alpha));
221 }
222
223 pub fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32) {
224 self.rects.push((color, x, y, w, h, 0.0, (true, true, true, true)));
225 }
226
227 /// [`PageContent::rect`] with a corner radius, applied only to `corners`
228 /// (top-left, top-right, bottom-right, bottom-left) — for a fill that has to
229 /// follow the rounded corner of the plate it sits inside.
230 pub fn rect_rounded(
231 &mut self,
232 color: [f32; 4],
233 x: f32,
234 y: f32,
235 w: f32,
236 h: f32,
237 radius: f32,
238 corners: (bool, bool, bool, bool),
239 ) {
240 self.rects.push((color, x, y, w, h, radius, corners));
241 }
242
243 /// A lit plate at (x, y, w, h): a rounded face in `color` plus the rolled,
244 /// lit perimeter — the treatment the pane plates wear. Unlike the relief
245 /// helpers this is NOT gated on `control_relief`: a plate owns the fill, so
246 /// skipping it would leave the surface unpainted rather than merely flat.
247 pub fn plate(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32) {
248 let depth = cce_ui::layout::bevel_width().min(h * 0.2);
249 self.plates.push((color, x, y, w, h, radius, depth));
250 }
251
252 /// Wall width for a carve whose corners are rounded at `radius`, capped so
253 /// the wall stays crease-free through the corner. A carve's wall straddles
254 /// the boundary, reaching `depth / 2` inward — and the inward offsets of a
255 /// squircle corner kink into a square crease past the corner's diagonal
256 /// curvature radius (`radius / corner_span_factor()`). The plate path
257 /// avoids this by widening its corner span instead, which a carve cannot:
258 /// its silhouette must stay on the widget's own nominal-radius corner.
259 /// Only tall rects ever feel the cap (a control's `h * 0.2` already lands
260 /// under it); the list well is the case that motivated it. Zero radius is
261 /// exempt — square corners meet in a miter by design.
262 fn carve_depth(h: f32, radius: f32) -> f32 {
263 let depth = cce_ui::layout::bevel_width().min(h * 0.2);
264 if radius > 0.0 {
265 depth.min(2.0 * radius / cce_ui::layout::corner_span_factor())
266 } else {
267 depth
268 }
269 }
270
271 /// A recessed well carved over the control at (x, y, w, h) — no-op when the
272 /// DE's control_relief styling is off.
273 pub fn relief_recessed(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32) {
274 if cce_ui::layout::control_relief() {
275 let depth = Self::carve_depth(h, radius);
276 self.reliefs.push((x, y, w, h, radius, depth, RELIEF_RECESSED));
277 }
278 }
279
280 /// [`Self::relief_recessed`] for the well holding pointer focus: the ring
281 /// replaces the lighting (see [`RELIEF_RECESSED_FOCUS`]).
282 pub fn relief_recessed_focused(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32) {
283 if cce_ui::layout::control_relief() {
284 let depth = Self::carve_depth(h, radius);
285 self.reliefs.push((x, y, w, h, radius, depth, RELIEF_RECESSED_FOCUS));
286 }
287 }
288
289 /// A raised plateau over the control at (x, y, w, h) — no-op when the DE's
290 /// control_relief styling is off.
291 pub fn relief_raised(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32) {
292 if cce_ui::layout::control_relief() {
293 let depth = Self::carve_depth(h, radius);
294 self.reliefs.push((x, y, w, h, radius, depth, RELIEF_RAISED));
295 }
296 }
297
298 /// A line engraved from `a` to `b` into the surface `host` — no-op when the
299 /// DE's control_relief styling is off.
300 pub fn groove(&mut self, a: (f32, f32), b: (f32, f32), width: f32, host: (f32, f32, f32, f32)) {
301 if cce_ui::layout::control_relief() {
302 let depth = cce_ui::layout::bevel_width().min(host.3 * 0.2);
303 self.grooves.push((a.0, a.1, b.0, b.1, width, depth, host.0, host.1, host.2, host.3));
304 }
305 }
306
307 /// A flush inset button plate (groove ring + beveled lip, face level with the
308 /// surface — the Button treatment) over the control at (x, y, w, h) — no-op
309 /// when the DE's control_relief styling is off.
310 pub fn relief_inset(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32) {
311 if cce_ui::layout::control_relief() {
312 let depth = Self::carve_depth(h, radius);
313 self.reliefs.push((x, y, w, h, radius, depth, RELIEF_INSET));
314 }
315 }
316
317 pub fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
318 self.texts.push((content.to_string(), size, x, y, color, None, None));
319 }
320
321 pub fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str) {
322 self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), None));
323 }
324
325 /// `text_with_font` with an explicit clip box `[l, t, r, b]` — for text
326 /// that scrolls under an edge and must render cut, not culled.
327 pub fn text_with_font_bounded(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str, bounds: [f32; 4]) {
328 self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), Some(bounds)));
329 }
330
331 pub fn button(
332 &mut self,
333 label: &str,
334 x: f32,
335 y: f32,
336 w: f32,
337 h: f32,
338 bg: [f32; 4],
339 hover_bg: [f32; 4],
340 label_color: [f32; 4],
341 action: crate::Message,
342 ) {
343 let btn = cce_ui::widget::Button::new(x, y, w, h)
344 .with_label(label)
345 .with_bg(bg)
346 .with_hover_bg(hover_bg)
347 .with_label_color(label_color);
348 self.buttons.push((btn, action));
349 }
350
351 /// A button wearing the toolkit's own face — no per-call colors. The
352 /// hand-tinted variant above predates the themed Button; chrome buttons
353 /// (the chooser's Cancel/Save) should look like every other DE button.
354 pub fn button_plain(&mut self, label: &str, x: f32, y: f32, w: f32, h: f32, action: crate::Message) {
355 // Colorless chrome: transparent face over the theme's border/relief —
356 // the closed-dropdown convention — with a faint neutral hover. The
357 // toolkit's Primary face is itself blue-tinted, which is exactly what
358 // these buttons are not supposed to be.
359 let btn = cce_ui::widget::Button::new(x, y, w, h)
360 .with_label(label)
361 .with_bg([0.0, 0.0, 0.0, 0.0])
362 .with_hover_bg([1.0, 1.0, 1.0, 0.10]);
363 self.buttons.push((btn, action));
364 }
365
366 pub fn button_left(
367 &mut self,
368 label: &str,
369 x: f32,
370 y: f32,
371 w: f32,
372 h: f32,
373 bg: [f32; 4],
374 hover_bg: [f32; 4],
375 label_color: [f32; 4],
376 action: crate::Message,
377 ) {
378 let btn = cce_ui::widget::Button::new(x, y, w, h)
379 .with_label(label)
380 .with_bg(bg)
381 .with_hover_bg(hover_bg)
382 .with_label_color(label_color)
383 .with_left_align(true);
384 self.buttons.push((btn, action));
385 }
386 }
387
388 impl RenderTarget for PageContent {
389 fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32) {
390 self.rects.push((color, x, y, w, h, 0.0, (true, true, true, true)));
391 }
392
393 fn rect_with_radius(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32) {
394 self.rects.push((color, x, y, w, h, radius, (true, true, true, true)));
395 }
396
397 fn rect_with_radius_corners(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, corners: (bool, bool, bool, bool)) {
398 self.rects.push((color, x, y, w, h, radius, corners));
399 }
400
401 fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
402 self.texts.push((content.to_string(), size, x, y, color, None, None));
403 }
404
405 fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str) {
406 self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), None));
407 }
408
409 fn text_with_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], bounds: Option<[f32; 4]>) {
410 self.texts.push((content.to_string(), size, x, y, color, None, bounds));
411 }
412
413 fn text_with_font_and_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str, bounds: Option<[f32; 4]>) {
414 self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), bounds));
415 }
416
417 /// The flat-path bridge for a widget's flush inset plate — the Dropdown's
418 /// OPEN popover, which is its trigger surface grown over the unified box.
419 /// Without this override the default degrades it to a plain rounded fill,
420 /// so the menu lost the groove ring and lip the closed trigger has (the
421 /// `dropdown_relief` carve) the moment it expanded.
422 ///
423 /// The face and the walls go to different vecs on purpose — `reliefs` are
424 /// edges-only, drawn over the faces `rects` own — and `display_list` emits
425 /// this part's rects before its reliefs, so one call here lands as fill
426 /// then ring, in that order, within whichever part is being collected.
427 ///
428 /// **The caller's `depth` is used verbatim, NOT re-derived from `h`.** The
429 /// widget computes it from the TRIGGER's height; the box handed here is the
430 /// trigger plus the revealed menu, several times taller. `relief_inset`
431 /// would recompute `bevel_width().min(h * 0.2)` off that expanded height
432 /// and thicken the ring as the menu grows — the same mistake
433 /// [`dropdown_relief`] documents. A ring that swells during the open
434 /// animation is exactly the artifact this override exists to avoid.
435 fn inset_plate(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32) {
436 self.rects.push((color, x, y, w, h, radius, (true, true, true, true)));
437 if cce_ui::layout::control_relief() {
438 self.reliefs.push((x, y, w, h, radius, depth, RELIEF_INSET));
439 }
440 }
441 /// The focused control plate's ring — same plate, rim lit.
442 fn inset_plate_tinted(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32, _tint: [f32; 3]) {
443 self.rects.push((color, x, y, w, h, radius, (true, true, true, true)));
444 if cce_ui::layout::control_relief() {
445 self.reliefs.push((x, y, w, h, radius, depth, RELIEF_INSET_FOCUS));
446 }
447 }
448 }