GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
The spacing ladder: root, pane and control rungs, and presets that pick one
Spacing lived as nine overlapping keys read two different ways — the
plate root's padding and gap on the style registry, plate_padding,
page_margin, column_gap, control_panel_{padding,gap} and grid_gap as
line-scraped statics that never saw a nested KDL key, and CONTROL_GAP a
literal — so every app named its own number. It is now one ladder:
root : root_plate_inset (roll + padding), root_plate_gap
pane : plate_padding, plate_gap (style.surface.plate.{padding,gap})
control: control_gap (style.control.gap)
plate_gap and control_gap are new keys, falling back to the root gap and
to CONTROL_GAP; the legacy keys are kept and honoured when set, and when
unset land on their rung (page_margin and control_panel_padding on the
pane padding, column_gap on the root gap, control_panel_gap on the pane
gap) — the radius rule, applied to spacing. All of them read the registry
first, so a nested config key works and live-reloads.
The box model gets the ladder as presets — Style::root_column/root_row,
pane_column/pane_row, controls_column/controls_row — so an app on the
standard plate picks a rung instead of naming a padding; the legacy
strategies' Defaults and ColumnLayout::pane/controls read the same
getters instead of the literal.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
src/layout.rs | 196 ++++++++++++++-----------------
src/scene/layout.rs | 45 +++++++
src/widget/container/container_layout.rs | 20 ++--
src/widget/layout_helper.rs | 12 ++
4 files changed, 155 insertions(+), 118 deletions(-)
diff --git a/src/layout.rs b/src/layout.rs
index e715d80..490dff3 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -296,6 +296,8 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
"style.surface.desktop.grid_cell_width" => "grid_cell_width",
"style.surface.desktop.grid_cell_height" => "grid_cell_height",
"style.surface.plate.padding" => "plate_padding",
+ "style.surface.plate.gap" => "plate_gap",
+ "style.control.gap" => "control_gap",
// The context menu's own radius (`menu_corner_radius`): a
// popover's corner is control-scale, not pane-scale.
"style.surface.menu.corner_radius" => "menu_corner_radius",
@@ -526,9 +528,9 @@ static RAMP_HEIGHT: RwLock<f32> = RwLock::new(32.0);
static BUTTON_STRIP_SPACING: RwLock<f32> = RwLock::new(8.0);
static SCROLLBAR_WIDTH: RwLock<f32> = RwLock::new(4.0);
static SCROLLBAR_INSET: RwLock<f32> = RwLock::new(16.0);
-static COLUMN_GAP: RwLock<f32> = RwLock::new(16.0);
-static CONTROL_PANEL_PADDING: RwLock<f32> = RwLock::new(16.0);
-static CONTROL_PANEL_GAP: RwLock<f32> = RwLock::new(12.0);
+static COLUMN_GAP: RwLock<Option<f32>> = RwLock::new(None);
+static CONTROL_PANEL_PADDING: RwLock<Option<f32>> = RwLock::new(None);
+static CONTROL_PANEL_GAP: RwLock<Option<f32>> = RwLock::new(None);
static TREE_OPACITY: RwLock<f32> = RwLock::new(1.0);
static TREE_BLUR: RwLock<f32> = RwLock::new(0.0);
@@ -667,7 +669,7 @@ pub fn reload_config() {
let val_str = rest.trim_end_matches('"').trim();
if let Ok(val) = val_str.parse::<f32>() {
if let Ok(mut lock) = PAGE_MARGIN.write() {
- *lock = val;
+ *lock = Some(val);
}
}
}
@@ -694,7 +696,7 @@ pub fn reload_config() {
let val_str = rest.trim_end_matches('"').trim();
if let Ok(val) = val_str.parse::<f32>() {
if let Ok(mut lock) = COLUMN_GAP.write() {
- *lock = val;
+ *lock = Some(val);
}
}
}
@@ -703,7 +705,7 @@ pub fn reload_config() {
let val_str = rest.trim_end_matches('"').trim();
if let Ok(val) = val_str.parse::<f32>() {
if let Ok(mut lock) = CONTROL_PANEL_PADDING.write() {
- *lock = val;
+ *lock = Some(val);
}
}
}
@@ -712,7 +714,7 @@ pub fn reload_config() {
let val_str = rest.trim_end_matches('"').trim();
if let Ok(val) = val_str.parse::<f32>() {
if let Ok(mut lock) = CONTROL_PANEL_GAP.write() {
- *lock = val;
+ *lock = Some(val);
}
}
}
@@ -1477,26 +1479,14 @@ pub fn set_nested_section_label_offset(offset: f32) {
}
+/// The pane rung's padding: from a pane plate's rim to its content, in
+/// logical px (`style.surface.plate.padding`). The second rung of the
+/// spacing ladder — [`root_plate_inset`] / [`root_plate_gap`] on the root
+/// plate, this and [`plate_gap`] inside a pane plate, [`control_gap`]
+/// between controls. Registry-backed (live-reloadable); the legacy flat
+/// `plate_padding = N` line still loads as a fallback.
pub fn plate_padding() -> f32 {
- use std::sync::Once;
- static INIT: Once = Once::new();
- INIT.call_once(|| {
- if let Some(content) = read_config() {
- for line in content.lines() {
- let trimmed = line.trim();
- if let Some(rest) = trimmed.strip_prefix("plate_padding") {
- let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
- let val_str = rest.trim_end_matches('"').trim();
- if let Ok(val) = val_str.parse::<f32>() {
- if let Ok(mut lock) = PLATE_PADDING.write() {
- *lock = val;
- }
- }
- }
- }
- }
- });
- *PLATE_PADDING.read().unwrap()
+ registry_float("plate_padding").unwrap_or_else(|| *PLATE_PADDING.read().unwrap())
}
pub fn set_plate_padding(padding: f32) {
@@ -1505,33 +1495,20 @@ pub fn set_plate_padding(padding: f32) {
}
}
-static PAGE_MARGIN: RwLock<f32> = RwLock::new(20.0);
+static PAGE_MARGIN: RwLock<Option<f32>> = RwLock::new(None);
+/// Legacy: the page-level margin (`style.surface.page.margin`). Unset, it
+/// IS the pane rung's [`plate_padding`] — a page is a pane — so an app
+/// still reading it lands on the ladder. Set, it is honoured as before.
pub fn page_margin() -> f32 {
- use std::sync::Once;
- static INIT: Once = Once::new();
- INIT.call_once(|| {
- if let Some(content) = read_config() {
- for line in content.lines() {
- let trimmed = line.trim();
- if let Some(rest) = trimmed.strip_prefix("page_margin") {
- let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
- let val_str = rest.trim_end_matches('"').trim();
- if let Ok(val) = val_str.parse::<f32>() {
- if let Ok(mut lock) = PAGE_MARGIN.write() {
- *lock = val;
- }
- }
- }
- }
- }
- });
- *PAGE_MARGIN.read().unwrap()
+ registry_float("page_margin")
+ .or_else(|| *PAGE_MARGIN.read().unwrap())
+ .unwrap_or_else(plate_padding)
}
pub fn set_page_margin(margin: f32) {
if let Ok(mut lock) = PAGE_MARGIN.write() {
- *lock = margin;
+ *lock = Some(margin);
}
}
@@ -1595,87 +1572,45 @@ pub fn set_grid_gap(gap: f32) {
}
}
+/// Legacy: the inter-column gap (`style.layout.column.gap`). Unset, it is
+/// the root plate's [`root_plate_gap`] — columns are siblings on the plate.
pub fn column_gap() -> f32 {
- use std::sync::Once;
- static INIT: Once = Once::new();
- INIT.call_once(|| {
- if let Some(content) = read_config() {
- for line in content.lines() {
- let trimmed = line.trim();
- if let Some(rest) = trimmed.strip_prefix("column_gap") {
- let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
- let val_str = rest.trim_end_matches('"').trim();
- if let Ok(val) = val_str.parse::<f32>() {
- if let Ok(mut lock) = COLUMN_GAP.write() {
- *lock = val;
- }
- }
- }
- }
- }
- });
- *COLUMN_GAP.read().unwrap()
+ registry_float("column_gap")
+ .or_else(|| *COLUMN_GAP.read().unwrap())
+ .unwrap_or_else(root_plate_gap)
}
pub fn set_column_gap(gap: f32) {
if let Ok(mut lock) = COLUMN_GAP.write() {
- *lock = gap;
+ *lock = Some(gap);
}
}
+/// Legacy: a control panel's padding (`style.control.control_panel.padding`).
+/// Unset, it is the pane rung's [`plate_padding`] — a control panel is a pane.
pub fn control_panel_padding() -> f32 {
- use std::sync::Once;
- static INIT: Once = Once::new();
- INIT.call_once(|| {
- if let Some(content) = read_config() {
- for line in content.lines() {
- let trimmed = line.trim();
- if let Some(rest) = trimmed.strip_prefix("control_panel_padding") {
- let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
- let val_str = rest.trim_end_matches('"').trim();
- if let Ok(val) = val_str.parse::<f32>() {
- if let Ok(mut lock) = CONTROL_PANEL_PADDING.write() {
- *lock = val;
- }
- }
- }
- }
- }
- });
- *CONTROL_PANEL_PADDING.read().unwrap()
+ registry_float("control_panel_padding")
+ .or_else(|| *CONTROL_PANEL_PADDING.read().unwrap())
+ .unwrap_or_else(plate_padding)
}
pub fn set_control_panel_padding(padding: f32) {
if let Ok(mut lock) = CONTROL_PANEL_PADDING.write() {
- *lock = padding;
+ *lock = Some(padding);
}
}
+/// Legacy: a control panel's gap (`style.control.control_panel.gap`).
+/// Unset, it is the pane rung's [`plate_gap`].
pub fn control_panel_gap() -> f32 {
- use std::sync::Once;
- static INIT: Once = Once::new();
- INIT.call_once(|| {
- if let Some(content) = read_config() {
- for line in content.lines() {
- let trimmed = line.trim();
- if let Some(rest) = trimmed.strip_prefix("control_panel_gap") {
- let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
- let val_str = rest.trim_end_matches('"').trim();
- if let Ok(val) = val_str.parse::<f32>() {
- if let Ok(mut lock) = CONTROL_PANEL_GAP.write() {
- *lock = val;
- }
- }
- }
- }
- }
- });
- *CONTROL_PANEL_GAP.read().unwrap()
+ registry_float("control_panel_gap")
+ .or_else(|| *CONTROL_PANEL_GAP.read().unwrap())
+ .unwrap_or_else(plate_gap)
}
pub fn set_control_panel_gap(gap: f32) {
if let Ok(mut lock) = CONTROL_PANEL_GAP.write() {
- *lock = gap;
+ *lock = Some(gap);
}
}
@@ -2248,6 +2183,30 @@ pub fn root_plate_inset() -> f32 {
bevel_width() + root_plate_padding()
}
+/// One style-registry float, initialising the registry on first use — the
+/// one read every rung getter goes through.
+fn registry_float(slot: &str) -> Option<f32> {
+ lazy_init_style_registry();
+ get_style_registry().read().unwrap().get_float(slot)
+}
+
+/// Gap between siblings INSIDE a pane plate, in logical px
+/// (`style.surface.plate.gap`) — the pane rung's twin of
+/// [`root_plate_gap`]. Unset, it is the root gap: one number reads as one
+/// rhythm across both rungs unless a config says otherwise.
+pub fn plate_gap() -> f32 {
+ registry_float("plate_gap").unwrap_or_else(root_plate_gap)
+}
+
+/// Gap between controls, in logical px (`style.control.gap`) — the control
+/// rung of the ladder: what the layout strategies put between a form's
+/// controls (and between a detached label's block and the next), in both
+/// axes. Unset, it is [`CONTROL_GAP`], one control height, the value every
+/// strategy's `Default` carried as a literal.
+pub fn control_gap() -> f32 {
+ registry_float("control_gap").unwrap_or(CONTROL_GAP)
+}
+
/// Roll-off width for the wall where a bar (menubar / status bar / the demo's
/// header band) steps down into the window plate. Wider than the plate's own
/// perimeter roll on purpose: the carve depth saturates at `bevel_width` in the
@@ -6821,11 +6780,32 @@ mod tests {
#[test]
fn test_column_gap() {
+ // A legacy key: set (by config or setter) it is honoured; unset it
+ // lands on the ladder — the root plate's gap.
let gap = column_gap();
- assert_eq!(gap, *super::COLUMN_GAP.read().unwrap());
+ match (registry_float("column_gap"), *super::COLUMN_GAP.read().unwrap()) {
+ (Some(v), _) | (None, Some(v)) => assert_eq!(gap, v),
+ (None, None) => assert_eq!(gap, root_plate_gap()),
+ }
assert!(gap.is_finite() && gap >= 0.0, "column gap {gap}");
}
+ #[test]
+ fn spacing_ladder_falls_back_rung_by_rung() {
+ // Every rung getter is finite and non-negative, and the inset is the
+ // roll plus the padding, whatever the config says.
+ for v in [root_plate_padding(), root_plate_gap(), root_plate_inset(), plate_padding(), plate_gap(), control_gap()] {
+ assert!(v.is_finite() && v >= 0.0, "{v}");
+ }
+ assert_eq!(root_plate_inset(), bevel_width() + root_plate_padding());
+ if registry_float("plate_gap").is_none() {
+ assert_eq!(plate_gap(), root_plate_gap());
+ }
+ if registry_float("control_gap").is_none() {
+ assert_eq!(control_gap(), CONTROL_GAP);
+ }
+ }
+
#[test]
fn test_print_fonts() {
let db = crate::widget::get_font_db();
diff --git a/src/scene/layout.rs b/src/scene/layout.rs
index 84e1136..3634de2 100644
--- a/src/scene/layout.rs
+++ b/src/scene/layout.rs
@@ -221,6 +221,51 @@ impl Style {
pub fn grid(columns: usize, col_gap: f32, row_gap: f32) -> Self {
Style { mode: LayoutMode::Grid(GridSpec { columns: columns.max(1), col_gap, row_gap }), ..Default::default() }
}
+ // ── The spacing ladder as presets ─────────────────────────────────
+ // An app on the standard root plate never names a padding or gap: it
+ // picks the rung. Root presets inset by `root_plate_inset` (the plate's
+ // roll plus one padding) and space siblings by `root_plate_gap`; pane
+ // presets by `plate_padding` / `plate_gap`; the controls presets space
+ // a form's controls by `control_gap` with no inset of their own, since
+ // they sit inside a pane or root preset that already has one.
+
+ /// A column of siblings standing on the root plate, stretched across it.
+ pub fn root_column() -> Self {
+ Self::column()
+ .padding(crate::layout::root_plate_inset())
+ .gap(crate::layout::root_plate_gap())
+ .cross_align(CrossAlign::Stretch)
+ }
+ /// A row of siblings standing on the root plate.
+ pub fn root_row() -> Self {
+ Self::row()
+ .padding(crate::layout::root_plate_inset())
+ .gap(crate::layout::root_plate_gap())
+ .cross_align(CrossAlign::Stretch)
+ }
+ /// A column inside a pane plate, inset from its rim.
+ pub fn pane_column() -> Self {
+ Self::column()
+ .padding(crate::layout::plate_padding())
+ .gap(crate::layout::plate_gap())
+ .cross_align(CrossAlign::Stretch)
+ }
+ /// A row inside a pane plate.
+ pub fn pane_row() -> Self {
+ Self::row()
+ .padding(crate::layout::plate_padding())
+ .gap(crate::layout::plate_gap())
+ .cross_align(CrossAlign::Stretch)
+ }
+ /// A column of controls: the control gap between them, no inset.
+ pub fn controls_column() -> Self {
+ Self::column().gap(crate::layout::control_gap())
+ }
+ /// A row of controls: the control gap between them, no inset.
+ pub fn controls_row() -> Self {
+ Self::row().gap(crate::layout::control_gap())
+ }
+
pub fn gap(mut self, v: f32) -> Self {
self.gap = v;
self
diff --git a/src/widget/container/container_layout.rs b/src/widget/container/container_layout.rs
index ffa61e8..3dd3668 100644
--- a/src/widget/container/container_layout.rs
+++ b/src/widget/container/container_layout.rs
@@ -140,7 +140,7 @@ impl Default for VerticalLayout {
Self {
padding_x: 0.0,
padding_y: 0.0,
- spacing: crate::layout::CONTROL_GAP,
+ spacing: crate::layout::control_gap(),
left: 0.0,
current_y: 0.0,
}
@@ -444,9 +444,9 @@ pub struct ColumnsLayout {
impl Default for ColumnsLayout {
fn default() -> Self {
Self {
- padding_x: crate::layout::CONTROL_GAP,
- padding_y: crate::layout::CONTROL_GAP,
- spacing: crate::layout::CONTROL_GAP,
+ padding_x: crate::layout::control_gap(),
+ padding_y: crate::layout::control_gap(),
+ spacing: crate::layout::control_gap(),
}
}
}
@@ -524,9 +524,9 @@ pub struct MosaicLayout {
impl Default for MosaicLayout {
fn default() -> Self {
Self {
- gap: crate::layout::CONTROL_GAP,
- padding_x: crate::layout::CONTROL_GAP,
- padding_y: crate::layout::CONTROL_GAP,
+ gap: crate::layout::control_gap(),
+ padding_x: crate::layout::control_gap(),
+ padding_y: crate::layout::control_gap(),
}
}
}
@@ -679,9 +679,9 @@ pub struct ReverseMosaicLayout {
impl Default for ReverseMosaicLayout {
fn default() -> Self {
Self {
- gap: crate::layout::CONTROL_GAP,
- padding_x: crate::layout::CONTROL_GAP,
- padding_y: crate::layout::CONTROL_GAP,
+ gap: crate::layout::control_gap(),
+ padding_x: crate::layout::control_gap(),
+ padding_y: crate::layout::control_gap(),
}
}
}
diff --git a/src/widget/layout_helper.rs b/src/widget/layout_helper.rs
index ce42c68..58c5289 100644
--- a/src/widget/layout_helper.rs
+++ b/src/widget/layout_helper.rs
@@ -21,6 +21,18 @@ impl ColumnLayout {
}
}
+ /// A column inside a pane plate: the pane rung's padding as the margin
+ /// and its gap between widgets (`plate_padding` / `plate_gap`).
+ pub fn pane(x: f32, y: f32, width: f32) -> Self {
+ Self::new(x, y, width, crate::layout::plate_gap(), crate::layout::plate_padding())
+ }
+
+ /// A column of controls with the control gap between them and no
+ /// margin of its own (`control_gap`).
+ pub fn controls(x: f32, y: f32, width: f32) -> Self {
+ Self::new(x, y, width, crate::layout::control_gap(), 0.0)
+ }
+
/// `height` is the CONTENT height; the widget's block adds its label strip.
pub fn add_widget(&mut self, widget: &mut dyn WidgetHost, height: f32) {
let total_h = height + widget.label_strip();