GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/bin/cce-ramp.rs (16.1K)
1 //! `cce-ramp` — a minimal popup hosting the [`Ramp`] widget in isolation, for
2 //! iterating on the widget's look without driving a full client around it.
3 //! `make install` puts it on PATH; run it inside a Wayland session. Edits print
4 //! their ramp spec to stdout, so the popup doubles as a curve scratchpad.
5 //!
6 //! `--key <dotted.key>` (with optional `--config <path>`, default the shared
7 //! config.kdl) turns the scratchpad into the `(ramp)` VALUE editor: the curve
8 //! seeds from that key's ramp spec, Save writes the spec back to that one key
9 //! with the `(ramp)` annotation, and Cancel closes without saving — the
10 //! `cce-relief --key` convention. cce-data-editor spawns it this way from a
11 //! ramp value's inline preview.
12 //!
13 //! Architecture mirrors the reference `DemoApp` (`src/main.rs`): display-list
14 //! frame, routed events, in-frame popovers.
15
16 use cce_ui::engine::{Application, EngineState, LogicalPosition, LogicalSize, WindowSettings};
17 use cce_ui::scene::layout::Rect;
18 use cce_ui::scene::paint::{DisplayList, PaintCtx};
19 use cce_ui::widget::{
20 Adapted, Button, ElementState, Event, KeyEvent, MouseButton, MouseScrollDelta, Ramp,
21 WidgetHost,
22 };
23 use wayland_client::QueueHandle;
24
25 /// Transparent rim between the surface edge and the plate: room for the ramp's
26 /// key pegs (r=28, +45 selected halo) to render outside the window frame
27 /// instead of being clipped at the buffer edge.
28 const OVERFLOW_MARGIN: f32 = 40.0;
29
30 #[derive(Debug, Clone)]
31 enum RampMsg {
32 Exit,
33 }
34
35 struct RampPopup {
36 ramp: Adapted<Ramp>,
37 /// Last spec printed to stdout — edits log their curve for copy/paste.
38 last_spec: String,
39 /// `--key` mode only; parked off-screen in the scratchpad.
40 save_button: Adapted<Button>,
41 cancel_button: Adapted<Button>,
42 /// `--key <dotted.key>`: Save writes the spec as a `(ramp)` value at
43 /// this key; the curve seeds from it. None = the stdout scratchpad.
44 target_key: Option<String>,
45 config_path: std::path::PathBuf,
46 /// Status line under the buttons (key mode): what the last save did.
47 status: String,
48 /// Set by the cancel click in `drain_changes` (no exit access there);
49 /// `handle_mouse_input` turns it into `RampMsg::Exit`.
50 exit_requested: bool,
51 ui_context: cce_ui::context::UiContext,
52 width: u32,
53 height: u32,
54 scale_factor: f64,
55 needs_rebuild: bool,
56 registered: bool,
57 }
58
59 impl RampPopup {
60 fn drain_changes(&mut self) {
61 let spec = self.ramp.inner().spec_string();
62 if spec != self.last_spec {
63 println!("{spec}");
64 self.last_spec = spec;
65 self.needs_rebuild = true;
66 }
67 if self.save_button.take_click() {
68 self.save_to_key();
69 self.needs_rebuild = true;
70 }
71 if self.cancel_button.take_click() {
72 // Discard-and-close: nothing persisted without Save.
73 self.exit_requested = true;
74 }
75 }
76
77 /// Persist the current curve as a `(ramp)` value at the target key.
78 fn save_to_key(&mut self) {
79 let Some(key) = self.target_key.clone() else { return };
80 let p = self.config_path.to_string_lossy().into_owned();
81 let spec = self.ramp.inner().spec_string();
82 let ok = cce_ui::config::write_config_value_typed(&p, &key, &spec, "style", Some("ramp"));
83 self.status = if ok {
84 println!("saved {key} -> {p}");
85 format!("Saved — {key} holds this curve.")
86 } else {
87 "Save FAILED — see config permissions.".to_string()
88 };
89 }
90 }
91
92 impl Application for RampPopup {
93 type Message = RampMsg;
94
95 fn new(
96 _qh: &QueueHandle<EngineState<Self>>,
97 _sender: calloop::channel::Sender<Self::Message>,
98 ) -> Self {
99 cce_ui::scale::set_scale_factor(1.0);
100 let mut ramp = Ramp::new();
101
102 // `--key <dotted.key>` / `--config <path>`: edit one `(ramp)` value
103 // in place — seed the curve from it, Save writes it back.
104 let mut config_path = cce_ui::config::get_config_path();
105 let mut target_key: Option<String> = None;
106 let args: Vec<String> = std::env::args().collect();
107 let mut i = 1;
108 while i < args.len() {
109 if args[i] == "--config" && i + 1 < args.len() {
110 config_path = std::path::PathBuf::from(&args[i + 1]);
111 i += 1;
112 } else if args[i] == "--key" && i + 1 < args.len() {
113 target_key = Some(args[i + 1].clone());
114 i += 1;
115 }
116 i += 1;
117 }
118 if let Some(key) = &target_key {
119 let seed = std::fs::read_to_string(&config_path)
120 .ok()
121 .map(|c| cce_ui::config::parse_kdl_to_json(&c))
122 .and_then(|v| v.pointer(&format!("/{}", key.replace('.', "/"))).cloned())
123 .and_then(|v| v.as_str().map(String::from));
124 if let Some(spec) = seed {
125 ramp.inner_mut().set_spec(&spec);
126 }
127 }
128
129 let last_spec = ramp.inner().spec_string();
130 Self {
131 ramp,
132 last_spec,
133 save_button: Button::new(0.0, 0.0, 0.0, 0.0).with_label("Save"),
134 cancel_button: Button::new(0.0, 0.0, 0.0, 0.0).with_label("Cancel"),
135 status: match &target_key {
136 Some(k) => format!("Edits are live in the curve; Save writes the {k} key."),
137 None => String::new(),
138 },
139 target_key,
140 config_path,
141 exit_requested: false,
142 ui_context: cce_ui::context::UiContext::new(),
143 width: 540,
144 height: 420,
145 scale_factor: 1.0,
146 needs_rebuild: true,
147 registered: false,
148 }
149 }
150
151 // Buffer-larger-than-geometry mode: the runner publishes the plate rect
152 // as the xdg window geometry + input region, so key pegs painted on the
153 // rim render outside the window frame and clicks there fall through.
154 fn overflow_margin(&self) -> u32 {
155 OVERFLOW_MARGIN as u32
156 }
157
158 fn settings(&self) -> WindowSettings {
159 let title = match &self.target_key {
160 Some(k) => {
161 let parts: Vec<&str> = k.split('.').collect();
162 format!("Ramp — {}", parts[parts.len().saturating_sub(2)..].join("."))
163 }
164 None => "Ramp".to_string(),
165 };
166 // The key mode adds a Save/Cancel row under the curve.
167 let extra = if self.target_key.is_some() { 56 } else { 0 };
168 WindowSettings {
169 title,
170 app_id: "cce-ramp".to_string(),
171 width: 460,
172 height: 340 + extra,
173 fullscreen: false,
174 min_size: Some((420, 300 + extra)),
175 }
176 }
177
178 fn update(&mut self, msg: Self::Message, _needs_rebuild: &mut bool, exit: &mut bool) {
179 match msg {
180 RampMsg::Exit => *exit = true,
181 }
182 }
183
184 fn tick(&mut self, dt: f32, needs_rebuild: &mut bool) {
185 if self.ui_context.tick(dt) {
186 // Tick-driven edits (hover-scroll glide) log their spec too.
187 self.drain_changes();
188 *needs_rebuild = true;
189 self.needs_rebuild = true;
190 }
191 }
192
193 fn display_list(&mut self, size: LogicalSize, scale: f64) -> Option<DisplayList> {
194 if !self.registered {
195 self.registered = true;
196 let w = self.ramp.as_ptr_mut();
197 let id = self.ramp.id();
198 self.ui_context.register_widget(id, w);
199 self.ui_context.register_widget(self.save_button.id(), self.save_button.as_ptr_mut());
200 self.ui_context.register_widget(self.cancel_button.id(), self.cancel_button.as_ptr_mut());
201 }
202
203 let size_changed = self.width != size.width as u32
204 || self.height != size.height as u32
205 || self.scale_factor != scale;
206 if self.needs_rebuild || size_changed {
207 self.width = size.width as u32;
208 self.height = size.height as u32;
209 self.scale_factor = scale;
210 cce_ui::scale::set_scale_factor(scale as f32);
211
212 // One widget, one rect: the ramp fills the plate inside half the
213 // DE pad (this popup runs tighter than a full client). The plate
214 // itself is inset by OVERFLOW_MARGIN so key pegs can render past
215 // the window frame into the transparent surface rim. Key mode
216 // reserves a Save/Cancel band under the curve.
217 let pad = OVERFLOW_MARGIN + cce_ui::layout::root_plate_padding() / 2.0;
218 let band = if self.target_key.is_some() { 56.0 } else { 0.0 };
219 self.ramp.set_rect(
220 pad,
221 pad,
222 (self.width as f32 - 2.0 * pad).max(0.0),
223 (self.height as f32 - 2.0 * pad - band).max(0.0),
224 );
225 if self.target_key.is_some() {
226 let by = self.height as f32 - pad - 30.0;
227 self.save_button.set_rect(pad, by, 96.0, 28.0);
228 self.cancel_button.set_rect(pad + 96.0 + 12.0, by, 96.0, 28.0);
229 } else {
230 self.save_button.set_rect(-1000.0, -1000.0, 1.0, 1.0);
231 self.cancel_button.set_rect(-1000.0, -1000.0, 1.0, 1.0);
232 }
233
234 self.needs_rebuild = false;
235 self.ui_context.rebuild_spatial_grid();
236 }
237
238 self.ui_context.clear_popovers();
239 if self.ramp.popover_rect().is_some() {
240 self.ui_context.register_popover(&mut self.ramp);
241 }
242
243 let mut pc = PaintCtx::new();
244 let (w, h) = (self.width as f32, self.height as f32);
245
246 // The window plate (the DemoApp idiom): page-low color at the configured
247 // opacity, config corner radius, rolled perimeter — inset by the
248 // overflow margin so widget content (the ramp's key pegs) can spill
249 // past the frame onto the transparent rim.
250 let mut plate = cce_ui::color::page_low_color();
251 if plate[3] > 0.001 {
252 // Half the DE opacity: this popup reads better mostly-glass.
253 plate[3] = cce_ui::color::root_plate_opacity() * 0.5;
254 }
255 let radius = cce_ui::colors::root_plate_corner_radius();
256 let bevel = cce_ui::layout::bevel_width();
257 let m = OVERFLOW_MARGIN;
258 pc.plate(
259 Rect { x: m, y: m, width: w - 2.0 * m, height: h - 2.0 * m },
260 (radius, radius, radius, radius),
261 &cce_ui::scene::Material::from_fill(plate),
262 bevel,
263 );
264
265 cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.ramp, &mut pc);
266 if self.target_key.is_some() {
267 cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.save_button, &mut pc);
268 cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.cancel_button, &mut pc);
269 if !self.status.is_empty() {
270 let pad = OVERFLOW_MARGIN + cce_ui::layout::root_plate_padding() / 2.0;
271 pc.text_with(
272 self.status.clone(),
273 pad + 2.0 * (96.0 + 12.0),
274 self.height as f32 - pad - 24.0,
275 12.0,
276 [0x9a, 0x9a, 0xa4],
277 None,
278 None,
279 );
280 }
281 }
282
283 // The ramp's field-dropdown popover, drawn into the frame on top.
284 if let Some((px, py, pw, ph)) = self.ramp.popover_rect() {
285 let mut coll = cce_ui::layout::PopoverCollector::new();
286 self.ramp.inner().preset_dropdown.render_popover(&mut coll);
287 self.ramp.inner().line_type_dropdown.render_popover(&mut coll);
288 for &(c, x, y, qw, qh) in &coll.rects {
289 pc.quad(Rect { x, y, width: qw, height: qh }, c);
290 }
291 let bounds = Some([px, py, px + pw, py + ph]);
292 for (content, size, tx, ty, color, font, _b) in coll.texts {
293 let color_u8 = [
294 (color[0] * 255.0).clamp(0.0, 255.0) as u8,
295 (color[1] * 255.0).clamp(0.0, 255.0) as u8,
296 (color[2] * 255.0).clamp(0.0, 255.0) as u8,
297 ];
298 pc.text_with(content, tx, ty, size, color_u8, font, bounds);
299 }
300 }
301
302 // The shared context menu (right-click on the graph), drawn last, on
303 // top of everything. Its labels carry the menu rect as bounds — the
304 // runner exempts them from the menu's own text occlusion that way.
305 // The lit plate and the menu font, in one call — the flat quads and
306 // a family-less label loop drew this menu square, opaque and in the
307 // default sans, unlike every app's.
308 cce_ui::widget::context_menu::paint_with_labels(&mut pc);
309
310 Some(pc.finish())
311 }
312
313 fn display_list_text(&self) -> bool {
314 true
315 }
316
317 fn ui_context(&self) -> Option<&cce_ui::context::UiContext> {
318 Some(&self.ui_context)
319 }
320
321 fn is_movable_root_plate_at(&self, px: f32, py: f32) -> bool {
322 self.ui_context.drag_allowed_at(px, py)
323 }
324
325 fn clear_color(&self) -> [f32; 4] {
326 [0.0, 0.0, 0.0, 0.0]
327 }
328
329 fn handle_pointer_move(&mut self, pos: LogicalPosition, needs_rebuild: &mut bool) {
330 if self.ui_context.cursor_moved_context_menu(pos.x, pos.y) {
331 *needs_rebuild = true;
332 self.needs_rebuild = true;
333 }
334 let ev = Event::PointerMove { x: pos.x, y: pos.y, local_x: pos.x, local_y: pos.y };
335 let changed = self.ui_context.propagate_event(&ev, self.ramp.id());
336 self.drain_changes();
337 if changed || self.needs_rebuild {
338 *needs_rebuild = true;
339 self.needs_rebuild = true;
340 }
341 }
342
343 fn handle_mouse_input(
344 &mut self,
345 button: MouseButton,
346 state: ElementState,
347 pos: LogicalPosition,
348 needs_rebuild: &mut bool,
349 ) -> Option<Self::Message> {
350 // The open context menu owns the press (item dispatch / dismiss).
351 if self.ui_context.mouse_input_context_menu(button, state, pos.x, pos.y) {
352 self.drain_changes();
353 *needs_rebuild = true;
354 self.needs_rebuild = true;
355 return None;
356 }
357 let ev = Event::MouseButton {
358 button,
359 state,
360 x: pos.x,
361 y: pos.y,
362 local_x: pos.x,
363 local_y: pos.y,
364 };
365 let mut changed = self.ui_context.propagate_event(&ev, self.ramp.id());
366 changed |= self.ui_context.propagate_event(&ev, self.save_button.id());
367 changed |= self.ui_context.propagate_event(&ev, self.cancel_button.id());
368 self.drain_changes();
369 if self.exit_requested {
370 return Some(RampMsg::Exit);
371 }
372 if changed || self.needs_rebuild {
373 *needs_rebuild = true;
374 self.needs_rebuild = true;
375 }
376 None
377 }
378
379 fn handle_mouse_wheel(
380 &mut self,
381 delta: &MouseScrollDelta,
382 pos: LogicalPosition,
383 needs_rebuild: &mut bool,
384 ) {
385 let ev = Event::MouseWheel {
386 delta: delta.clone(),
387 x: pos.x,
388 y: pos.y,
389 local_x: pos.x,
390 local_y: pos.y,
391 };
392 let changed = self.ui_context.propagate_event(&ev, self.ramp.id());
393 self.drain_changes();
394 if changed || self.needs_rebuild {
395 *needs_rebuild = true;
396 self.needs_rebuild = true;
397 }
398 }
399
400 fn handle_key_input(
401 &mut self,
402 event: &KeyEvent,
403 needs_rebuild: &mut bool,
404 ) -> Option<Self::Message> {
405 use cce_ui::widget::{Key, NamedKey};
406 if event.state == ElementState::Pressed {
407 if event.logical_key == Key::Named(NamedKey::Escape) {
408 return Some(RampMsg::Exit);
409 }
410 if event.ctrl {
411 if let Key::Character(ref c) = event.logical_key {
412 if c == "q" {
413 return Some(RampMsg::Exit);
414 }
415 }
416 }
417 }
418 let ev = Event::KeyInput(event.clone());
419 let handled = self.ui_context.propagate_event(&ev, self.ramp.id());
420 self.drain_changes();
421 if handled || self.needs_rebuild {
422 *needs_rebuild = true;
423 self.needs_rebuild = true;
424 }
425 None
426 }
427 }
428
429 fn main() {
430 cce_ui::engine::run::<RampPopup>();
431 }