git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commit79962502fa87b9169cc8fa8e1d4c44a2aed35ac6
parenta66e9c57c9
authorLucas Galante <[email protected]>
date2026-09-24 16:54
feat: the Alt+D dialog compresses its backdrop harder than the panes

The dialog plate drew through append_widget_plate, whose material is the
fill's own, so it carried the plates' compression — 0 in a config that
keeps the panes clear — and its rows sat on the full-contrast scene. It now
paints its plate with its own material at State::dialog_compression:
style.surface.dialog.compression in config.kdl, 0.8 when unset, above a
menu's 0.6. Opaque plates ignore it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

 CLAUDE.md     | 10 ++++++++++
 src/app.rs    | 18 ++++++++++++++++++
 src/dialog.rs | 19 +++++++++++++++++++
 src/main.rs   | 19 +++++++++++++++++++
 src/render.rs | 28 ++++++++++++++++++++++++----
 5 files changed, 90 insertions(+), 4 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 47a644d..b6af66b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1592,6 +1592,16 @@ sees one (`input.kdl`'s `cce-window-manager` domain has `super+d` on the app
 launcher), and Super held is the DE's window-adjust modifier besides. Alt is the
 app's own — the `move_*` family already lives there.
 
+**The dialog plate compresses its backdrop harder than anything else**
+(`State::dialog_compression`, `style.surface.dialog.compression` in
+config.kdl, default `DIALOG_COMPRESSION` 0.8 against a menu's 0.6). The
+plate's own compression can be 0 — a config that keeps the panes clear —
+and at that value the rows sat on the full-contrast scene and network. The
+render arm paints the plate itself with `dialog::plate_material` rather
+than through `append_widget_plate`, whose material comes from the fill alone,
+exactly as the node bodies do for `node_compression`. Opaque plates (blur
+off) have no backdrop and ignore it.
+
 **The dialog is painted after the overlay passes, not in the widget walk.** A
 high `z_order` is not enough: `append_frame_text`, `append_scale_readout` and the
 point-number overlay all run AFTER the whole walk, so the graph's node labels drew
diff --git a/src/app.rs b/src/app.rs
index 90bf050..ff33f00 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1910,6 +1910,13 @@ pub struct State {
     /// pull the view toward the tint's key and read as solid. None = the
     /// pane's.
     pub node_compression: Option<f32>,
+    /// The Alt+D dialog's backdrop compression (`style.surface.dialog.
+    /// compression`, 0..1; [`crate::dialog::DIALOG_COMPRESSION`] when
+    /// unset). The dialog is a modal read over whatever the scene and the
+    /// network are doing, so it pulls its backdrop toward the tint harder
+    /// than a pane or even a menu does — at the plate's own compression the
+    /// rows sat on a busy, full-contrast picture.
+    pub dialog_compression: f32,
     pub last_design_mod_time: Option<std::time::SystemTime>,
     pub last_config_mod_time: Option<std::time::SystemTime>,
     pub floating_network_layout: (f32, f32, f32, f32),
@@ -5107,6 +5114,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             network_opacity: 0.95,
             node_opacity: 1.0,
             node_compression: None,
+            dialog_compression: crate::dialog::DIALOG_COMPRESSION,
             last_design_mod_time: {
                 let design_path = DesignSettings::file_path();
                 std::fs::metadata(&design_path).and_then(|m| m.modified()).ok()
@@ -5505,12 +5513,22 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             .and_then(|v| v.as_f64())
             .map(|k| (k as f32).clamp(0.0, 1.0));
 
+        let dialog_compression = cce_ui::config::cached_config()
+            .pointer("/style/surface/dialog/compression")
+            .and_then(|v| v.as_f64())
+            .map(|k| (k as f32).clamp(0.0, 1.0))
+            .unwrap_or(crate::dialog::DIALOG_COMPRESSION);
+
         let mut changed = false;
 
         if self.node_compression != node_compression {
             self.node_compression = node_compression;
             changed = true;
         }
+        if self.dialog_compression != dialog_compression {
+            self.dialog_compression = dialog_compression;
+            changed = true;
+        }
         
         if (self.network_opacity - opacity).abs() > 0.001 {
             self.network_opacity = opacity;
diff --git a/src/dialog.rs b/src/dialog.rs
index 48a32fb..401d414 100644
--- a/src/dialog.rs
+++ b/src/dialog.rs
@@ -188,6 +188,25 @@ const READOUT_GAP: f32 = 8.0;
 /// Gap between the query line and the list.
 const GAP: f32 = 8.0;
 
+/// The dialog plate's backdrop compression when `style.surface.dialog.
+/// compression` is unset — above a menu's 0.6, since a modal is read over
+/// the whole busy window rather than beside the one control that opened it.
+/// Compression pulls what shows through the frost toward the tint's key, so
+/// the rows read against an even ground whatever is behind them.
+pub const DIALOG_COMPRESSION: f32 = 0.8;
+
+/// The dialog plate's material: the param plate's fill, frosted as the
+/// plates are, with the compression raised to `compression`. An opaque
+/// plate (blur off in the config) has no backdrop to compress and is
+/// returned as it is.
+pub fn plate_material(compression: f32) -> cce_ui::scene::Material {
+    let mut m = cce_ui::scene::Material::from_fill(colors::param_plate_fill());
+    if let cce_ui::scene::Frost::Frosted { compression: c, .. } = &mut m.frost {
+        *c = compression.clamp(0.0, 1.0);
+    }
+    m
+}
+
 /// The dialog's rect inside a `width` x `height` window: centered
 /// horizontally, and a little above centre vertically so the list grows into
 /// the window's roomier half rather than down over the status bar.
diff --git a/src/main.rs b/src/main.rs
index 05edda4..5fa0f33 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1384,6 +1384,25 @@ mod tests {
         assert_eq!(m.match_command(&plain, &Key::Named(NamedKey::ArrowDown)), Some("play_pause_reverse"));
     }
 
+    /// The dialog plate carries its own backdrop compression, above a
+    /// menu's: whatever the plates' own is (0 in a config that keeps the
+    /// panes clear), the modal pulls its backdrop toward the tint, and a
+    /// value out of range is clamped rather than overshooting.
+    #[test]
+    fn the_dialog_plate_compresses_its_backdrop_harder_than_a_menu() {
+        use cce_ui::scene::Frost;
+        assert!(crate::dialog::DIALOG_COMPRESSION > cce_ui::color::menu_compression().min(0.6));
+        for (asked, want) in [(0.8, 0.8), (1.7, 1.0), (-0.2, 0.0)] {
+            match crate::dialog::plate_material(asked).frost {
+                Frost::Frosted { compression, .. } => assert!((compression - want).abs() < 1e-6, "{asked} -> {compression}"),
+                // Blur off: nothing behind the plate to compress.
+                Frost::Opaque => {}
+            }
+        }
+        let state = State::new(false);
+        assert!((0.0..=1.0).contains(&state.dialog_compression));
+    }
+
     /// Ctrl+Up rewinds: a moving timeline stops and the playhead lands on
     /// the start frame, from wherever it was and in either direction; from a
     /// stop it is simply a jump to the start.
diff --git a/src/render.rs b/src/render.rs
index d3bbf2f..2523076 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -280,10 +280,30 @@ impl State {
             // Modern-paint surface, the playbar's contract: the designer
             // authors the plate (the dialog floats, so the pane radii and the
             // focus tint do not apply — it is never a pane and never the
-            // focused one), then Dialog::paint emits the tab strip, the query
-            // line and the rows. A subtree painter, so append_frame_text skips
-            // the slot and the chord column keeps its own font and bounds.
-            append_widget_plate(w, pc);
+            // focused one), then Dialog::paint emits the query line and the
+            // rows. A subtree painter, so append_frame_text skips the slot and
+            // the chord column keeps its own font and bounds.
+            //
+            // The plate is `append_widget_plate`'s, drawn here rather than by
+            // it because that helper builds its material from the fill alone
+            // and the dialog wants its own backdrop compression
+            // (`State::dialog_compression`) — the node bodies' override, for
+            // the same reason: the one knob that differs from the panes.
+            let (x, y, ww, h) = w.rect();
+            let r = rect(x, y, ww, h);
+            let cr = w.corner_radii();
+            let radii = (cr.top_left, cr.top_right, cr.bottom_right, cr.bottom_left);
+            let mat = crate::dialog::plate_material(self.dialog_compression);
+            match w.solid_border() {
+                Some(_) if cce_ui::layout::control_relief() => {
+                    pc.bevel(r, radii, &mat, cce_ui::colors::plate_bevel_width());
+                }
+                Some((border, thickness)) => {
+                    pc.fill_material(r, radii, &mat);
+                    pc.border(r, radii, [0.0; 4], border, thickness);
+                }
+                None => pc.fill_material(r, radii, &mat),
+            }
             w.paint_self(&self.ui_context, pc);
         } else if idx == PLAYBAR_IDX {
             // Modern-paint pane: the plate from the legacy views like the other