git.lucas.co / cce-files
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 }