git.lucas.co / cce-layout-interface
page layout tool
git clone https://git.lucas.co/cce-layout-interface.git

commit08856d0bb79a38b3f44cc90677bb32d82e39ff84
parent183d91797b
authorLucas Galante <[email protected]>
date2026-09-24 14:51
Spacing: the ladder, not literals

The sidebar and the canvas now stand on the root plate the way the
toolkit's standard says: inset from the window edge by root_plate_inset(),
separated by root_plate_gap(), and the sidebar's sections use the pane
rung inside. One size stays named — SIDEBAR_W (280) — and everything that
used to repeat it derives from sidebar_rect() / canvas_rect() /
page_origin(), which paint and the pointer handlers both read.

- `280.0` x 12 (paginator rect, canvas quad, divider, page centring at
  five sites, three pointer-handler splits) -> SIDEBAR_W via the helpers;
  the canvas quad and paginator rect are inset by root_plate_inset() on
  every window side, the canvas one root_plate_gap() right of the sidebar
- `cy = 16.0` (sections' top) -> sidebar top + plate_padding()
- `cw - 40.0` x 6 -> Section::content_width(); `sec.ax(12.0)` ->
  Section::content_left(); the `12.0` x_off passed to Section::widget /
  text -> Section::DEFAULT_MARGIN_X (the toolkit's own name for it)
- `sec.spacing(12.0)` x 57 and the Geometry section's `16.0` x 3, the
  layer list's `8.0` -> plate_gap()
- recent-files rows: `+ 4.0` / `- 16.0` -> the list's edge_inset and the
  scrollbar lane (scrollbar_width() + edge_inset)
- "No recent files" placeholder `+ 12.0`/`+ 16.0` -> plate_padding()
- divider: a hairline centred in the root gap, the sidebar's height
- hit-testing: `px < 280.0` -> `px < canvas_rect().0`, so the split is
  the painted canvas edge
- deliberate: the 8.0/6.0 between the caption lines of "No Selection"
- TODO(style): the "Recent Files" heading's 16.0 line height; the ruler's
  20/12/6 band and tick sizes; the page-space `8.0 * zoom` label offsets

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

 src/main.rs | 380 +++++++++++++++++++++++++++++++++---------------------------
 1 file changed, 207 insertions(+), 173 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index bfd5d8e..efcf4d2 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -9,7 +9,13 @@ use cce_ui::widget::{
     TextBox, Slider, TextLabel, Paginator, Button, Dropdown, Toggle, ColorSelector,
     Label, Spinbox, Key, FontSelector, PageSelector, MenuController
 };
-use cce_ui::layout::{RenderTarget, Section, UiFrame};
+use cce_ui::layout::{plate_gap, plate_padding, root_plate_gap, root_plate_inset, RenderTarget, Section, UiFrame};
+
+/// The sidebar's width — a size, not a spacing, and the one place it is
+/// named: the paginator's rect, the canvas's left edge and the pointer
+/// handlers' sidebar/canvas split all derive from it (`sidebar_rect`,
+/// `canvas_rect`).
+const SIDEBAR_W: f32 = 280.0;
 
 pub struct PageContent {
     pub rects: Vec<([f32; 4], f32, f32, f32, f32)>,
@@ -695,6 +701,33 @@ impl LayoutApp {
         self.slider_zoom.get_scaled_value() * zoom_multiplier
     }
 
+    /// The sidebar's rect `(x, y, w, h)`: standing on the root plate, inset
+    /// from the window edge on its three outer sides, `SIDEBAR_W` wide.
+    fn sidebar_rect(&self) -> (f32, f32, f32, f32) {
+        let inset = root_plate_inset();
+        (inset, inset, SIDEBAR_W, self.height as f32 - 2.0 * inset)
+    }
+
+    /// The canvas's rect `(x, y, w, h)`: the sidebar's sibling on the root
+    /// plate, one root gap to its right and inset from the other three edges.
+    /// Paint and hit-testing both read it, so the two cannot drift.
+    fn canvas_rect(&self) -> (f32, f32, f32, f32) {
+        let inset = root_plate_inset();
+        let x = inset + SIDEBAR_W + root_plate_gap();
+        (x, inset, self.width as f32 - x - inset, self.height as f32 - 2.0 * inset)
+    }
+
+    /// Top-left of the page sheet: centred on the canvas at the render zoom,
+    /// then panned.
+    fn page_origin(&self) -> (f32, f32) {
+        let zoom = self.render_zoom();
+        let (cx, cy, cw, ch) = self.canvas_rect();
+        (
+            cx + (cw - self.page_w * zoom) / 2.0 + self.pan_x,
+            cy + (ch - self.page_h * zoom) / 2.0 + self.pan_y,
+        )
+    }
+
     fn rebuild_text_items(&mut self) {
         let font_system = &mut self.font_system;
         let selected_page = self.paginator.selected_page();
@@ -809,25 +842,30 @@ impl LayoutApp {
         let mut pc = PageContent::new();
         let ui_frame = UiFrame::start(0.0);
 
-        let sidebar_w = self.paginator.sidebar_w();
-        let cx = sidebar_w;
-        let cy = 16.0;
-        let cw = 280.0 - sidebar_w;
+        // The tab strip takes the left of the sidebar and the selected page's
+        // sections the rest, starting one pane padding below the sidebar's top.
+        let (side_x, side_y, side_w, _) = self.sidebar_rect();
+        let strip_w = self.paginator.sidebar_w();
+        let cx = side_x + strip_w;
+        let cy = side_y + plate_padding();
+        let cw = side_w - strip_w;
 
         match self.paginator.selected_page() {
             0 => {
                 let mut sec = Section::new(&mut pc, cx, cy, cw, "File Operations");
-                let col_w = cw - 40.0;
-                sec.widget(&mut pc, &mut self.btn_new_doc, 12.0, col_w, 26.0, &mut self.ui_context);
-                sec.spacing(12.0);
-                sec.widget(&mut pc, &mut self.btn_open, 12.0, col_w, 26.0, &mut self.ui_context);
-                sec.spacing(12.0);
-
-                sec.text(&mut pc, "Recent Files", 12.0, 0.0, 11.0, [0.83, 0.83, 0.83, 1.0]);
+                let col_w = sec.content_width();
+                sec.widget(&mut pc, &mut self.btn_new_doc, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                sec.spacing(plate_gap());
+                sec.widget(&mut pc, &mut self.btn_open, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                sec.spacing(plate_gap());
+
+                sec.text(&mut pc, "Recent Files", Section::DEFAULT_MARGIN_X, 0.0, 11.0, [0.83, 0.83, 0.83, 1.0]);
+                // TODO(style): the heading's line height, not a gap — the 11px
+                // label plus the room under it before the well.
                 sec.spacing(16.0);
 
                 let list_h = 100.0;
-                let list_x = sec.ax(12.0);
+                let list_x = sec.content_left();
                 let list_y = sec.ay();
                 self.recent_files_list.set_rect(list_x, list_y, col_w, list_h);
                 self.recent_files_list.push_prims(&mut pc);
@@ -836,8 +874,11 @@ impl LayoutApp {
                 self.recent_files_list.update_bounds(self.recent_files.len(), list_y, list_h);
 
                 let btn_h = 22.0;
-                let inner_x = list_x + 4.0;
-                let inner_w = col_w - 16.0;
+                // Rows hug the well's border by the same inset its scrollbar
+                // keeps from the frame, and leave the bar's lane on the right.
+                let hug = self.recent_files_list.edge_inset;
+                let inner_x = list_x + hug;
+                let inner_w = col_w - hug - (cce_ui::layout::scrollbar_width() + hug);
 
                 // `get_item_draw_y` returns PARTIALLY visible rows (toolkit
                 // ScrollRegion intersection contract); the scoped clip clamps
@@ -864,58 +905,58 @@ impl LayoutApp {
                 pc.active_clip = None;
 
                 if self.recent_files.is_empty() {
-                    pc.text("No recent files", list_x + 12.0, list_y + 16.0, 11.0, [0.55, 0.55, 0.60, 1.0]);
+                    pc.text("No recent files", list_x + plate_padding(), list_y + plate_padding(), 11.0, [0.55, 0.55, 0.60, 1.0]);
                 }
 
-                sec.spacing(12.0);
-                sec.widget(&mut pc, &mut self.btn_save, 12.0, col_w, 26.0, &mut self.ui_context);
-                sec.spacing(12.0);
-                sec.widget(&mut pc, &mut self.btn_save_as, 12.0, col_w, 26.0, &mut self.ui_context);
-                sec.spacing(12.0);
-                sec.widget(&mut pc, &mut self.btn_exit, 12.0, col_w, 26.0, &mut self.ui_context);
-                sec.spacing(12.0);
+                sec.spacing(plate_gap());
+                sec.widget(&mut pc, &mut self.btn_save, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                sec.spacing(plate_gap());
+                sec.widget(&mut pc, &mut self.btn_save_as, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                sec.spacing(plate_gap());
+                sec.widget(&mut pc, &mut self.btn_exit, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                sec.spacing(plate_gap());
                 sec.finish(&mut pc);
             }
             1 => {
                 let mut sec = Section::new(&mut pc, cx, cy, cw, "Document Size");
-                let col_w = cw - 40.0;
-                sec.widget(&mut pc, &mut self.dropdown_presets, 12.0, col_w, 26.0, &mut self.ui_context);
-                sec.spacing(12.0);
-                sec.widget(&mut pc, &mut self.slider_page_x, 12.0, col_w, 18.0, &mut self.ui_context);
-                sec.spacing(12.0);
-                sec.widget(&mut pc, &mut self.slider_page_y, 12.0, col_w, 18.0, &mut self.ui_context);
-                sec.spacing(12.0);
-                sec.widget(&mut pc, &mut self.page_color_selector, 12.0, col_w, 26.0, &mut self.ui_context);
-                sec.spacing(12.0);
+                let col_w = sec.content_width();
+                sec.widget(&mut pc, &mut self.dropdown_presets, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                sec.spacing(plate_gap());
+                sec.widget(&mut pc, &mut self.slider_page_x, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
+                sec.spacing(plate_gap());
+                sec.widget(&mut pc, &mut self.slider_page_y, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
+                sec.spacing(plate_gap());
+                sec.widget(&mut pc, &mut self.page_color_selector, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                sec.spacing(plate_gap());
                 let next_y = sec.finish(&mut pc);
 
                 let mut sec2 = Section::new(&mut pc, cx, next_y, cw, "Margins & Mode");
-                sec2.widget(&mut pc, &mut self.toggle_margin, 12.0, col_w, 26.0, &mut self.ui_context);
-                sec2.spacing(12.0);
-                sec2.widget(&mut pc, &mut self.dropdown_margin_units, 12.0, col_w, 26.0, &mut self.ui_context);
-                sec2.spacing(12.0);
-                sec2.widget(&mut pc, &mut self.slider_margin_x, 12.0, col_w, 18.0, &mut self.ui_context);
-                sec2.spacing(12.0);
-                sec2.widget(&mut pc, &mut self.slider_margin_y, 12.0, col_w, 18.0, &mut self.ui_context);
-                sec2.spacing(12.0);
-                sec2.widget(&mut pc, &mut self.toggle_word_processor, 12.0, col_w, 26.0, &mut self.ui_context);
-                sec2.spacing(12.0);
+                sec2.widget(&mut pc, &mut self.toggle_margin, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                sec2.spacing(plate_gap());
+                sec2.widget(&mut pc, &mut self.dropdown_margin_units, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                sec2.spacing(plate_gap());
+                sec2.widget(&mut pc, &mut self.slider_margin_x, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
+                sec2.spacing(plate_gap());
+                sec2.widget(&mut pc, &mut self.slider_margin_y, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
+                sec2.spacing(plate_gap());
+                sec2.widget(&mut pc, &mut self.toggle_word_processor, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                sec2.spacing(plate_gap());
                 sec2.finish(&mut pc);
             }
             2 => {
-                let col_w = cw - 40.0;
                 if self.word_processor_enabled {
                     let mut sec = Section::new(&mut pc, cx, cy, cw, "Word Processor");
-                    sec.widget(&mut pc, &mut self.font_selector, 12.0, col_w, 26.0, &mut self.ui_context);
-                    sec.spacing(12.0);
-                    sec.widget(&mut pc, &mut self.sidebar_size, 12.0, col_w, 26.0, &mut self.ui_context);
-                    sec.spacing(12.0);
-                    sec.widget(&mut pc, &mut self.slider_r, 12.0, col_w, 18.0, &mut self.ui_context);
-                    sec.spacing(12.0);
-                    sec.widget(&mut pc, &mut self.slider_g, 12.0, col_w, 18.0, &mut self.ui_context);
-                    sec.spacing(12.0);
-                    sec.widget(&mut pc, &mut self.slider_b, 12.0, col_w, 18.0, &mut self.ui_context);
-                    sec.spacing(12.0);
+                    let col_w = sec.content_width();
+                    sec.widget(&mut pc, &mut self.font_selector, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                    sec.spacing(plate_gap());
+                    sec.widget(&mut pc, &mut self.sidebar_size, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                    sec.spacing(plate_gap());
+                    sec.widget(&mut pc, &mut self.slider_r, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
+                    sec.spacing(plate_gap());
+                    sec.widget(&mut pc, &mut self.slider_g, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
+                    sec.spacing(plate_gap());
+                    sec.widget(&mut pc, &mut self.slider_b, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
+                    sec.spacing(plate_gap());
                     sec.finish(&mut pc);
                 } else if let Some(idx) = self.selected_idx {
                     let (is_text, is_vector) = match &self.elements[idx] {
@@ -925,139 +966,143 @@ impl LayoutApp {
                     };
 
                     let mut sec = Section::new(&mut pc, cx, cy, cw, "Geometry");
-                    sec.widget(&mut pc, &mut self.sidebar_x, 12.0, col_w, 26.0, &mut self.ui_context);
-                    sec.spacing(16.0);
-                    sec.widget(&mut pc, &mut self.sidebar_y, 12.0, col_w, 26.0, &mut self.ui_context);
-                    sec.spacing(16.0);
-                    sec.widget(&mut pc, &mut self.sidebar_w, 12.0, col_w, 26.0, &mut self.ui_context);
-                    sec.spacing(16.0);
-                    sec.widget(&mut pc, &mut self.sidebar_h, 12.0, col_w, 26.0, &mut self.ui_context);
-                    sec.spacing(12.0);
+                    let col_w = sec.content_width();
+                    sec.widget(&mut pc, &mut self.sidebar_x, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                    sec.spacing(plate_gap());
+                    sec.widget(&mut pc, &mut self.sidebar_y, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                    sec.spacing(plate_gap());
+                    sec.widget(&mut pc, &mut self.sidebar_w, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                    sec.spacing(plate_gap());
+                    sec.widget(&mut pc, &mut self.sidebar_h, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                    sec.spacing(plate_gap());
                     let next_y = sec.finish(&mut pc);
 
                     let mut sec_align = Section::new(&mut pc, cx, next_y, cw, "Alignment");
-                    sec_align.widget(&mut pc, &mut self.dropdown_align_h, 12.0, col_w, 26.0, &mut self.ui_context);
-                    sec_align.spacing(12.0);
-                    sec_align.widget(&mut pc, &mut self.dropdown_align_v, 12.0, col_w, 26.0, &mut self.ui_context);
+                    sec_align.widget(&mut pc, &mut self.dropdown_align_h, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                    sec_align.spacing(plate_gap());
+                    sec_align.widget(&mut pc, &mut self.dropdown_align_v, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
                     let next_y = sec_align.finish(&mut pc);
 
                     if is_text {
                         let mut sec2 = Section::new(&mut pc, cx, next_y, cw, "Text Properties");
-                        sec2.widget(&mut pc, &mut self.sidebar_text, 12.0, col_w, 26.0, &mut self.ui_context);
-                        sec2.spacing(12.0);
-                        sec2.widget(&mut pc, &mut self.toggle_multiline, 12.0, col_w, 26.0, &mut self.ui_context);
-                        sec2.spacing(12.0);
-                        sec2.widget(&mut pc, &mut self.font_selector, 12.0, col_w, 26.0, &mut self.ui_context);
-                        sec2.spacing(12.0);
-                        sec2.widget(&mut pc, &mut self.sidebar_size, 12.0, col_w, 26.0, &mut self.ui_context);
-                        sec2.spacing(12.0);
-                        sec2.widget(&mut pc, &mut self.slider_r, 12.0, col_w, 18.0, &mut self.ui_context);
-                        sec2.spacing(12.0);
-                        sec2.widget(&mut pc, &mut self.slider_g, 12.0, col_w, 18.0, &mut self.ui_context);
-                        sec2.spacing(12.0);
-                        sec2.widget(&mut pc, &mut self.slider_b, 12.0, col_w, 18.0, &mut self.ui_context);
-                        sec2.spacing(12.0);
+                        sec2.widget(&mut pc, &mut self.sidebar_text, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                        sec2.spacing(plate_gap());
+                        sec2.widget(&mut pc, &mut self.toggle_multiline, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                        sec2.spacing(plate_gap());
+                        sec2.widget(&mut pc, &mut self.font_selector, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                        sec2.spacing(plate_gap());
+                        sec2.widget(&mut pc, &mut self.sidebar_size, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                        sec2.spacing(plate_gap());
+                        sec2.widget(&mut pc, &mut self.slider_r, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
+                        sec2.spacing(plate_gap());
+                        sec2.widget(&mut pc, &mut self.slider_g, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
+                        sec2.spacing(plate_gap());
+                        sec2.widget(&mut pc, &mut self.slider_b, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
+                        sec2.spacing(plate_gap());
                         let next_y_align = sec2.finish(&mut pc);
 
                         let mut sec_text_align = Section::new(&mut pc, cx, next_y_align, cw, "Text Alignment");
-                        sec_text_align.widget(&mut pc, &mut self.dropdown_text_align_h, 12.0, col_w, 26.0, &mut self.ui_context);
-                        sec_text_align.spacing(12.0);
-                        sec_text_align.widget(&mut pc, &mut self.dropdown_text_align_v, 12.0, col_w, 26.0, &mut self.ui_context);
+                        sec_text_align.widget(&mut pc, &mut self.dropdown_text_align_h, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                        sec_text_align.spacing(plate_gap());
+                        sec_text_align.widget(&mut pc, &mut self.dropdown_text_align_v, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
                         sec_text_align.finish(&mut pc);
                     } else if is_vector {
                         let mut sec2 = Section::new(&mut pc, cx, next_y, cw, "Line Properties");
-                        sec2.widget(&mut pc, &mut self.sidebar_size, 12.0, col_w, 26.0, &mut self.ui_context);
-                        sec2.spacing(12.0);
-                        sec2.widget(&mut pc, &mut self.dropdown_line_cap, 12.0, col_w, 26.0, &mut self.ui_context);
-                        sec2.spacing(12.0);
-                        sec2.widget(&mut pc, &mut self.slider_r, 12.0, col_w, 18.0, &mut self.ui_context);
-                        sec2.spacing(12.0);
-                        sec2.widget(&mut pc, &mut self.slider_g, 12.0, col_w, 18.0, &mut self.ui_context);
-                        sec2.spacing(12.0);
-                        sec2.widget(&mut pc, &mut self.slider_b, 12.0, col_w, 18.0, &mut self.ui_context);
-                        sec2.spacing(12.0);
+                        sec2.widget(&mut pc, &mut self.sidebar_size, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                        sec2.spacing(plate_gap());
+                        sec2.widget(&mut pc, &mut self.dropdown_line_cap, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                        sec2.spacing(plate_gap());
+                        sec2.widget(&mut pc, &mut self.slider_r, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
+                        sec2.spacing(plate_gap());
+                        sec2.widget(&mut pc, &mut self.slider_g, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
+                        sec2.spacing(plate_gap());
+                        sec2.widget(&mut pc, &mut self.slider_b, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
+                        sec2.spacing(plate_gap());
                         sec2.finish(&mut pc);
                     } else {
                         let mut sec2 = Section::new(&mut pc, cx, next_y, cw, "Fill Color");
-                        sec2.widget(&mut pc, &mut self.slider_r, 12.0, col_w, 18.0, &mut self.ui_context);
-                        sec2.spacing(12.0);
-                        sec2.widget(&mut pc, &mut self.slider_g, 12.0, col_w, 18.0, &mut self.ui_context);
-                        sec2.spacing(12.0);
-                        sec2.widget(&mut pc, &mut self.slider_b, 12.0, col_w, 18.0, &mut self.ui_context);
-                        sec2.spacing(12.0);
+                        sec2.widget(&mut pc, &mut self.slider_r, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
+                        sec2.spacing(plate_gap());
+                        sec2.widget(&mut pc, &mut self.slider_g, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
+                        sec2.spacing(plate_gap());
+                        sec2.widget(&mut pc, &mut self.slider_b, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
+                        sec2.spacing(plate_gap());
                         sec2.finish(&mut pc);
                     }
                 } else {
                     let mut sec = Section::new(&mut pc, cx, cy, cw, "Properties");
-                    sec.widget(&mut pc, &mut self.label_sel_status, 12.0, col_w, 18.0, &mut self.ui_context);
+                    let col_w = sec.content_width();
+                    sec.widget(&mut pc, &mut self.label_sel_status, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
+                    // style: deliberate — the lines of one caption, set tight,
+                    // not siblings on the pane.
                     sec.spacing(8.0);
-                    sec.widget(&mut pc, &mut self.label_sel_desc1, 12.0, col_w, 14.0, &mut self.ui_context);
+                    sec.widget(&mut pc, &mut self.label_sel_desc1, Section::DEFAULT_MARGIN_X, col_w, 14.0, &mut self.ui_context);
                     sec.spacing(6.0);
-                    sec.widget(&mut pc, &mut self.label_sel_desc2, 12.0, col_w, 14.0, &mut self.ui_context);
-                    sec.spacing(12.0);
+                    sec.widget(&mut pc, &mut self.label_sel_desc2, Section::DEFAULT_MARGIN_X, col_w, 14.0, &mut self.ui_context);
+                    sec.spacing(plate_gap());
                     sec.finish(&mut pc);
                 }
             }
             3 => {
                 let mut sec = Section::new(&mut pc, cx, cy, cw, "Add Elements");
-                let col_w = cw - 40.0;
-                sec.widget(&mut pc, &mut self.btn_add_text, 12.0, col_w, 26.0, &mut self.ui_context);
-                sec.spacing(12.0);
-                sec.widget(&mut pc, &mut self.btn_add_rect, 12.0, col_w, 26.0, &mut self.ui_context);
-                sec.spacing(12.0);
-                sec.widget(&mut pc, &mut self.btn_add_banner, 12.0, col_w, 26.0, &mut self.ui_context);
-                sec.spacing(12.0);
-                sec.widget(&mut pc, &mut self.btn_add_vector, 12.0, col_w, 26.0, &mut self.ui_context);
-                sec.spacing(12.0);
-                sec.widget(&mut pc, &mut self.label_total_elements, 12.0, col_w, 18.0, &mut self.ui_context);
-                sec.spacing(12.0);
+                let col_w = sec.content_width();
+                sec.widget(&mut pc, &mut self.btn_add_text, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                sec.spacing(plate_gap());
+                sec.widget(&mut pc, &mut self.btn_add_rect, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                sec.spacing(plate_gap());
+                sec.widget(&mut pc, &mut self.btn_add_banner, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                sec.spacing(plate_gap());
+                sec.widget(&mut pc, &mut self.btn_add_vector, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                sec.spacing(plate_gap());
+                sec.widget(&mut pc, &mut self.label_total_elements, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
+                sec.spacing(plate_gap());
                 sec.finish(&mut pc);
             }
             4 => {
                 let mut sec = Section::new(&mut pc, cx, cy, cw, "Grid");
-                let col_w = cw - 40.0;
-                sec.widget(&mut pc, &mut self.toggle_grid, 12.0, col_w, 26.0, &mut self.ui_context);
-                sec.spacing(12.0);
-                sec.widget(&mut pc, &mut self.grid_color_selector, 12.0, col_w, 26.0, &mut self.ui_context);
-                sec.spacing(12.0);
-                sec.widget(&mut pc, &mut self.spinbox_grid_size, 12.0, col_w, 26.0, &mut self.ui_context);
-                sec.spacing(12.0);
-                sec.widget(&mut pc, &mut self.label_grid_snap, 12.0, col_w, 26.0, &mut self.ui_context);
-                sec.spacing(12.0);
-                sec.widget(&mut pc, &mut self.dropdown_grid_units, 12.0, col_w, 26.0, &mut self.ui_context);
-                sec.spacing(12.0);
+                let col_w = sec.content_width();
+                sec.widget(&mut pc, &mut self.toggle_grid, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                sec.spacing(plate_gap());
+                sec.widget(&mut pc, &mut self.grid_color_selector, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                sec.spacing(plate_gap());
+                sec.widget(&mut pc, &mut self.spinbox_grid_size, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                sec.spacing(plate_gap());
+                sec.widget(&mut pc, &mut self.label_grid_snap, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                sec.spacing(plate_gap());
+                sec.widget(&mut pc, &mut self.dropdown_grid_units, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                sec.spacing(plate_gap());
                 let next_y = sec.finish(&mut pc);
 
                 let mut sec_zoom = Section::new(&mut pc, cx, next_y, cw, "Zoom");
-                sec_zoom.widget(&mut pc, &mut self.slider_zoom, 12.0, col_w, 18.0, &mut self.ui_context);
-                sec_zoom.spacing(12.0);
+                sec_zoom.widget(&mut pc, &mut self.slider_zoom, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
+                sec_zoom.spacing(plate_gap());
                 let next_y = sec_zoom.finish(&mut pc);
 
                 let mut sec_margins = Section::new(&mut pc, cx, next_y, cw, "Margins");
-                sec_margins.widget(&mut pc, &mut self.margin_color_selector, 12.0, col_w, 26.0, &mut self.ui_context);
-                sec_margins.spacing(12.0);
-                sec_margins.widget(&mut pc, &mut self.spinbox_margin_thickness, 12.0, col_w, 26.0, &mut self.ui_context);
-                sec_margins.spacing(12.0);
+                sec_margins.widget(&mut pc, &mut self.margin_color_selector, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                sec_margins.spacing(plate_gap());
+                sec_margins.widget(&mut pc, &mut self.spinbox_margin_thickness, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                sec_margins.spacing(plate_gap());
                 let next_y = sec_margins.finish(&mut pc);
 
                 let mut sec2 = Section::new(&mut pc, cx, next_y, cw, "Rulers");
-                sec2.widget(&mut pc, &mut self.toggle_rulers, 12.0, col_w, 26.0, &mut self.ui_context);
-                sec2.spacing(12.0);
-                sec2.widget(&mut pc, &mut self.dropdown_units, 12.0, col_w, 26.0, &mut self.ui_context);
-                sec2.spacing(12.0);
+                sec2.widget(&mut pc, &mut self.toggle_rulers, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                sec2.spacing(plate_gap());
+                sec2.widget(&mut pc, &mut self.dropdown_units, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                sec2.spacing(plate_gap());
                 sec2.finish(&mut pc);
             }
             5 => {
                 let mut sec = Section::new(&mut pc, cx, cy, cw, "Layers List");
-                let col_w = cw - 40.0;
+                let col_w = sec.content_width();
                 if self.layer_buttons.is_empty() {
                     let mut label_no_layers = Label::new("No elements found").with_color([0x83, 0x83, 0x8a]);
-                    sec.widget(&mut pc, &mut label_no_layers, 12.0, col_w, 18.0, &mut self.ui_context);
-                    sec.spacing(12.0);
+                    sec.widget(&mut pc, &mut label_no_layers, Section::DEFAULT_MARGIN_X, col_w, 18.0, &mut self.ui_context);
+                    sec.spacing(plate_gap());
                 } else {
                     for btn in &mut self.layer_buttons {
-                        sec.widget(&mut pc, &mut **btn, 12.0, col_w, 26.0, &mut self.ui_context);
-                        sec.spacing(8.0);
+                        sec.widget(&mut pc, &mut **btn, Section::DEFAULT_MARGIN_X, col_w, 26.0, &mut self.ui_context);
+                        sec.spacing(plate_gap());
                     }
                 }
                 sec.finish(&mut pc);
@@ -1113,13 +1158,8 @@ impl LayoutApp {
 
         let mut labels = Vec::new();
 
-        let canvas_w = self.width as f32 - 280.0;
-        let canvas_h = self.height as f32;
         let zoom = self.render_zoom();
-        let page_w = self.page_w * zoom;
-        let page_h = self.page_h * zoom;
-        let page_x = 280.0 + (canvas_w - page_w) / 2.0 + self.pan_x;
-        let page_y = (canvas_h - page_h) / 2.0 + self.pan_y;
+        let (page_x, page_y) = self.page_origin();
 
         // 2. Paginator sidebar tabs: emitted in display_list via the paint walk.
 
@@ -1171,6 +1211,10 @@ impl LayoutApp {
 
 
         // 5. Canvas Element Labels (drawn relative to the paper sheet)
+        // TODO(style): the `8.0 * zoom` / `6.0 * zoom` label offsets and the
+        // text box's `16.0`/`12.0` insets are page-space padding inside a
+        // document element — they scale with the zoom, so they are content,
+        // not UI spacing.
         if self.word_processor_enabled {
             // Word-processor text box: emitted in display_list via the paint walk.
         } else {
@@ -3278,7 +3322,8 @@ impl Application for LayoutApp {
 
             // Set paginator bounds in the sidebar region
             cce_ui::scale::set_scale_factor(scale as f32);
-            self.paginator.set_rect(0.0, 0.0, 280.0, size.height as f32);
+            let (side_x, side_y, side_w, side_h) = self.sidebar_rect();
+            self.paginator.set_rect(side_x, side_y, side_w, side_h);
 
             self.rebuild_layers_tab_widgets();
             self.rebuild_text_items();
@@ -3288,22 +3333,22 @@ impl Application for LayoutApp {
 
 
         // 3. Render Canvas & centered paper sheet
-        let canvas_w = self.width as f32 - 280.0;
-        let canvas_h = self.height as f32;
+        let (canvas_x, canvas_y, canvas_w, canvas_h) = self.canvas_rect();
         let zoom = self.render_zoom();
 
         // Centered Paper Position
-        let page_x = 280.0 + (canvas_w - self.page_w * zoom) / 2.0 + self.pan_x;
-        let page_y = (canvas_h - self.page_h * zoom) / 2.0 + self.pan_y;
+        let (page_x, page_y) = self.page_origin();
 
         // Dark slate canvas backdrop
         let mut canvas_bg = [0.12, 0.12, 0.15, 1.0];
         if let Some(opacity) = cce_ui::color::read_opacity_if_configured() {
             canvas_bg[3] = opacity;
         }
-        quads.push((280.0, 0.0, canvas_w, canvas_h, canvas_bg));
+        quads.push((canvas_x, canvas_y, canvas_w, canvas_h, canvas_bg));
 
         // Axis Rulers
+        // TODO(style): the 20px band, the 12/6 tick lengths and the corner
+        // block are the ruler's own geometry, not spacing on a plate.
         if self.toggle_rulers.toggled() {
             let ruler_bg = [0.18, 0.18, 0.22, 1.0];
             let tick_color = [0.45, 0.45, 0.50, 0.8];
@@ -3499,16 +3544,17 @@ impl Application for LayoutApp {
         }
         quads.extend(self.sidebar_quads.iter().cloned());
 
-        // Divider between Sidebar and Canvas
-        quads.push((280.0, 0.0, 1.0, self.height as f32, [0.20, 0.20, 0.25, 1.0]));
+        // Divider between Sidebar and Canvas: a hairline down the middle of
+        // the root gap that separates them.
+        {
+            let (canvas_x, canvas_y, _, canvas_h) = self.canvas_rect();
+            quads.push((canvas_x - root_plate_gap() / 2.0, canvas_y, 1.0, canvas_h, [0.20, 0.20, 0.25, 1.0]));
+        }
 
         // Vectors (the legacy view_vectors body), after plain geometry as the wrapper ordered.
         if !self.word_processor_enabled {
             let zoom = self.render_zoom();
-            let canvas_w = self.width as f32 - 280.0;
-            let canvas_h = self.height as f32;
-            let page_x = 280.0 + (canvas_w - self.page_w * zoom) / 2.0 + self.pan_x;
-            let page_y = (canvas_h - self.page_h * zoom) / 2.0 + self.pan_y;
+            let (page_x, page_y) = self.page_origin();
             for element in &self.elements {
                 if let Element::Vector { x1, y1, x2, y2, stroke_width, color, line_cap } = element {
                     let vx1 = page_x + *x1 * zoom;
@@ -3559,8 +3605,7 @@ impl Application for LayoutApp {
         let px = pos.x as f32;
         let py = pos.y as f32;
 
-        let sidebar_w = 280.0;
-        if px < sidebar_w {
+        if px < self.canvas_rect().0 {
             if {
                 // Self-routing composite: handle_event, not propagate — the router's
                 // children-first descent would let the embedded strip consume this.
@@ -3582,13 +3627,8 @@ impl Application for LayoutApp {
             if self.active_page_widgets_cursor_moved(px, py) { changed = true; }
 
             // Compute centering coordinates for canvas elements
-            let canvas_w = self.width as f32 - 280.0;
-            let canvas_h = self.height as f32;
             let zoom = self.render_zoom();
-            let page_w = self.page_w * zoom;
-            let page_h = self.page_h * zoom;
-            let page_x = 280.0 + (canvas_w - page_w) / 2.0 + self.pan_x;
-            let page_y = (canvas_h - page_h) / 2.0 + self.pan_y;
+            let (page_x, page_y) = self.page_origin();
 
             let cx = (px - page_x) / zoom;
             let cy = (py - page_y) / zoom;
@@ -3644,8 +3684,7 @@ impl Application for LayoutApp {
         let px = pos.x as f32;
         let py = pos.y as f32;
 
-        let sidebar_w = 280.0;
-        if px < sidebar_w {
+        if px < self.canvas_rect().0 {
             let pag_mouse = {
                 // Self-routing composite: handle_event, not propagate (see pointer move).
                 let ev = cce_ui::widget::Event::MouseButton { button, state, x: px, y: py, local_x: px, local_y: py };
@@ -3754,13 +3793,8 @@ impl Application for LayoutApp {
             }
         } else {
             // Compute centering coordinates for canvas elements
-            let canvas_w = self.width as f32 - 280.0;
-            let canvas_h = self.height as f32;
             let zoom = self.render_zoom();
-            let page_w = self.page_w * zoom;
-            let page_h = self.page_h * zoom;
-            let page_x = 280.0 + (canvas_w - page_w) / 2.0 + self.pan_x;
-            let page_y = (canvas_h - page_h) / 2.0 + self.pan_y;
+            let (page_x, page_y) = self.page_origin();
 
             let cx = (px - page_x) / zoom;
             let cy = (py - page_y) / zoom;
@@ -3837,7 +3871,7 @@ impl Application for LayoutApp {
     fn handle_mouse_wheel(&mut self, delta: &MouseScrollDelta, pos: LogicalPosition, needs_rebuild: &mut bool) {
         let px = pos.x as f32;
         let py = pos.y as f32;
-        if px < 280.0 {
+        if px < self.canvas_rect().0 {
             let mut wheel_handled = false;
             if self.active_page_widgets_mouse_wheel(delta, px, py) {
                 wheel_handled = true;