git.lucas.co / cce-text-editor
text editor
git clone https://git.lucas.co/cce-text-editor.git

commitce6b2d6d234e0d6f264a160d36caf9b4c36674a3
parent3dba2564c4
authorLucas Galante <[email protected]>
date2026-09-24 14:47
Spacing: the ladder, not literals

The solver's containers read the DE-wide rungs instead of naming numbers:

- top bar `Edges { left: 10, right: 10, top: 8, bottom: 8 }` -> horizontal
  `root_plate_inset()`, vertical centring (`CrossAlign::Center`) in the
  band, `root_plate_gap()` between the bar's items.
- content `.padding(10.0)` -> `root_plate_inset()` at the sides (window
  edge), `root_plate_gap()` above and below (editor vs each band, siblings
  on the root plate).
- status text x `15.0` -> `root_plate_inset()`.
- the menu's placeholder rect `(10, 8)` -> `root_plate_inset()`, centred.
- band heights 42/30 are sizes, not spacing: named `MENUBAR_H` /
  `STATUSBAR_H` and kept.

The root column carries no inset because both bands are flush to the
window edge by design; the rung is applied to what stands between them.
Left as TODO(style): the bands as flat fills rather than `recess_edges`
carves (a look change, not a spacing one), and the file label's absolute
x = 420.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

 src/main.rs | 66 +++++++++++++++++++++++++++++++++++++++++++------------------
 1 file changed, 47 insertions(+), 19 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index a9b1904..ddd95c7 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,6 +6,11 @@ use cce_ui::widget::{
     TextBox, Key, Dropdown
 };
 
+/// The menubar and status bands' heights (sizes, not spacing — the ladder
+/// supplies the insets and gaps around and inside them).
+const MENUBAR_H: f32 = 42.0;
+const STATUSBAR_H: f32 = 30.0;
+
 #[derive(Debug, Clone)]
 enum AppMessage {
     Exit,
@@ -151,6 +156,8 @@ impl TextEditorApp {
         } else {
             file_name_str
         };
+        // TODO(style): an absolute x for the file label; it should sit one
+        // `root_plate_gap` after the menu's solved rect, like a toolbar sibling.
         pc.text_with(format!("File: {}", display_name), 420.0, 15.0, 12.0, [0xdd, 0xdd, 0xe2], mono(), None);
 
         // 2. Status Bar indicators
@@ -170,7 +177,7 @@ impl TextEditorApp {
         }
         pc.text_with(
             format!("Line: {}, Col: {} | Length: {} chars", logical_line, logical_col, text_src.chars().count()),
-            15.0,
+            cce_ui::layout::root_plate_inset(),
             self.height as f32 - 20.0,
             11.0,
             [0x83, 0x83, 0x8a],
@@ -214,7 +221,9 @@ impl Application for TextEditorApp {
             "Exit".to_string(),
         ];
         let mut menu_dropdown = Dropdown::new(dropdown_options, 0).with_custom_display_text("File");
-        menu_dropdown.set_rect(10.0, 8.0, 70.0, 26.0);
+        // Placeholder rect until the first frame's layout solve assigns the real one:
+        // inset from the window edge by the root rung, centred in the 42px menubar band.
+        menu_dropdown.set_rect(cce_ui::layout::root_plate_inset(), (MENUBAR_H - 26.0) / 2.0, 70.0, 26.0);
 
         // Monospace textbox setup
         let mut editor = TextBox::new(String::new())
@@ -365,29 +374,43 @@ impl Application for TextEditorApp {
             self.scale_factor = scale;
 
             // Layout via the scene solver (Phase 6ab — the routed-events/scene-layout
-            // reference): the frame is a stretched column [top bar (fixed 42, padded
-            // 10/8, holding the fixed menu leaf), content (grow, padded 10, holding the
-            // editor), status bar (fixed 30)]. Solves to the exact legacy rects
-            // (menu 10,8 70x26; editor 10,52 (w-20)x(h-92)) with the mins the old
-            // hand-math clamped by.
+            // reference): the frame is a stretched column [menubar band (fixed
+            // MENUBAR_H, holding the fixed menu leaf), content (grow, holding the
+            // editor), status band (fixed STATUSBAR_H)].
+            //
+            // Spacing by rung, never by number. The root column itself carries no
+            // inset: both bands are flush to the window edge by design (the
+            // MenuBar/StatusBar band idiom), so the root rung is applied to what
+            // stands between and inside them instead — the menu and the editor
+            // inset from the window's sides by `root_plate_inset`, and the editor
+            // stands off each band by `root_plate_gap`, the gap between siblings
+            // on the root plate. The menu centres in its band rather than carrying
+            // a vertical padding.
             {
                 use cce_ui::scene::arena::Arena;
                 use cce_ui::scene::layout::{
                     compute_layout, CrossAlign, Edges, LayoutBox, Length, Size as LSize, Style,
                 };
+                let inset = cce_ui::layout::root_plate_inset();
+                let gap = cce_ui::layout::root_plate_gap();
                 let mut arena: Arena<LayoutBox> = Arena::new();
                 let root = arena.insert(LayoutBox::container(
                     Style::column().cross_align(CrossAlign::Stretch),
                 ));
                 let top_bar = arena.insert(LayoutBox::container({
-                    let mut s = Style::row().height(Length::Fixed(42.0));
-                    s.padding = Edges { left: 10.0, right: 10.0, top: 8.0, bottom: 8.0 };
+                    let mut s = Style::row()
+                        .height(Length::Fixed(MENUBAR_H))
+                        .gap(gap)
+                        .cross_align(CrossAlign::Center);
+                    s.padding = Edges { left: inset, right: inset, top: 0.0, bottom: 0.0 };
                     s
                 }));
                 let menu = arena.insert(LayoutBox::leaf(Style::row(), LSize::new(70.0, 26.0)));
-                let content = arena.insert(LayoutBox::container(
-                    Style::column().grow(1.0).padding(10.0).cross_align(CrossAlign::Stretch),
-                ));
+                let content = arena.insert(LayoutBox::container({
+                    let mut s = Style::column().grow(1.0).cross_align(CrossAlign::Stretch);
+                    s.padding = Edges { left: inset, right: inset, top: gap, bottom: gap };
+                    s
+                }));
                 let editor = arena.insert(LayoutBox::container({
                     let mut s = Style::column().grow(1.0);
                     s.min_width = 100.0;
@@ -395,7 +418,7 @@ impl Application for TextEditorApp {
                     s
                 }));
                 let status = arena.insert(LayoutBox::container(
-                    Style::column().height(Length::Fixed(30.0)),
+                    Style::column().height(Length::Fixed(STATUSBAR_H)),
                 ));
                 arena.append_child(root, top_bar);
                 arena.append_child(top_bar, menu);
@@ -428,19 +451,24 @@ impl Application for TextEditorApp {
         let mut pc = cce_ui::scene::paint::PaintCtx::new();
         let w = self.width as f32;
         let h = self.height as f32;
-        let status_y = h - 30.0;
+        let status_y = h - STATUSBAR_H;
         // The standard root plate (cce-ui PlateSpec::window).
         pc.root_plate(w, h);
         // Silhouette radius (cce-ui RFC 7b): matches the compositor clip.
+        // TODO(style): these bands are flat fills over the plate; the toolkit's
+        // idiom for a menubar/status band is a carve (`recess_edges` with
+        // `bar_wall_width`, one wall facing the content, as cce-ui's demo does).
+        // Switching drops the bands' own fill colour, so it is a look change,
+        // not a spacing one, and is left for a deliberate pass.
         let radius = cce_ui::layout::window_silhouette_radius();
         if radius > 0.1 {
-            pc.rounded_rect(Rect { x: 0.0, y: 0.0, width: w, height: 42.0 }, radius, (true, true, false, false), [0.08, 0.08, 0.12, 1.0]);
-            pc.rounded_rect(Rect { x: 0.0, y: status_y, width: w, height: 30.0 }, radius, (false, false, true, true), [0.08, 0.08, 0.10, 1.0]);
+            pc.rounded_rect(Rect { x: 0.0, y: 0.0, width: w, height: MENUBAR_H }, radius, (true, true, false, false), [0.08, 0.08, 0.12, 1.0]);
+            pc.rounded_rect(Rect { x: 0.0, y: status_y, width: w, height: STATUSBAR_H }, radius, (false, false, true, true), [0.08, 0.08, 0.10, 1.0]);
         } else {
-            pc.quad(Rect { x: 0.0, y: 0.0, width: w, height: 42.0 }, [0.08, 0.08, 0.12, 1.0]);
-            pc.quad(Rect { x: 0.0, y: status_y, width: w, height: 30.0 }, [0.08, 0.08, 0.10, 1.0]);
+            pc.quad(Rect { x: 0.0, y: 0.0, width: w, height: MENUBAR_H }, [0.08, 0.08, 0.12, 1.0]);
+            pc.quad(Rect { x: 0.0, y: status_y, width: w, height: STATUSBAR_H }, [0.08, 0.08, 0.10, 1.0]);
         }
-        pc.quad(Rect { x: 0.0, y: 42.0, width: w, height: 1.0 }, [0.18, 0.18, 0.22, 1.0]);
+        pc.quad(Rect { x: 0.0, y: MENUBAR_H, width: w, height: 1.0 }, [0.18, 0.18, 0.22, 1.0]);
         pc.quad(Rect { x: 0.0, y: status_y, width: w, height: 1.0 }, [0.18, 0.18, 0.22, 1.0]);
 
         self.push_chrome_text(&mut pc);