graphic design tool
git clone https://git.lucas.co/cce-designer.git
fix: the palette's zoom band runs out to the chord column's edge
The band began SLIDER_W in from the row's right end and stopped a readout
lane short of it, which left it ending well inside the key bindings listed
on every other row -- a control that looked unfinished rather than aligned.
It now runs from that same left edge out to the CHORD column's right edge,
so it ends exactly where the key bindings end and the switch column stays
clear. That edge moves with `toggle_col`, so the three rects become methods
on the roster rather than associated functions of the rect.
The percentage readout moves AHEAD of the band, the one place left for it,
and a press now tests the band alone rather than the whole control: over
the lane a click on the readout would have jumped the value to whichever
end of the range it abuts.
Co-Authored-By: Claude Opus 5 <[email protected]>
CLAUDE.md | 15 ++++++++----
src/dialog.rs | 74 +++++++++++++++++++++++++++++++++++++++--------------------
src/main.rs | 54 ++++++++++++++++++++++++++++++++++++++++---
3 files changed, 111 insertions(+), 32 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 2799f14..362d3bb 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -926,10 +926,17 @@ because the miss is silent — the row just ships plain.
network pane is focused — and only then, since zoom is that pane's — the
list heads with a "Zoom" row (`ZOOM_ROW_ID`) carrying a `slider`: the
toolkit's own `Slider`, painted from one stamp over the row's right end (it
-borrows the chord column rather than reserving `SLIDER_W` on every row),
-reading the network zoom as a percentage of the configured grid pitch
-(`State::zoom_percent`, 100 = Reset Zoom, range the pitch limits). A press
-on the band jumps to it and arms the app's widget-drag protocol on
+borrows the chord column rather than reserving `SLIDER_W` on every row). The
+band **begins `SLIDER_W` in from the row's right end and runs out to the CHORD
+column's right edge** — so it ends where every other row's key binding ends
+rather than stopping short of them, and the switch column stays clear; that
+edge moves with `toggle_col`, which is why the rects are methods rather than
+associated functions. The percentage readout therefore sits AHEAD of the band,
+the one place left for it, and a press tests the band alone — over the whole
+control a click on the readout would jump the value to whichever end of the
+range it abuts. It reads the network zoom as a percentage of the configured
+grid pitch (`State::zoom_percent`, 100 = Reset Zoom, range the pitch limits).
+A press on the band jumps to it and arms the app's widget-drag protocol on
`DIALOG_IDX` (`Dialog::draggable` / `drag_*`, exactly as the Settings
half's sliders arm it on `DIALOG_PARAMS_IDX`), so the value follows the
pointer off the plate; the drained value lands through
diff --git a/src/dialog.rs b/src/dialog.rs
index c7a997c..6517ec5 100644
--- a/src/dialog.rs
+++ b/src/dialog.rs
@@ -113,12 +113,16 @@ pub const SWATCH_SIDE: f32 = 14.0;
/// pane's toggles have.
pub const TOGGLE_W: f32 = 36.0;
const TOGGLE_H: f32 = ROW_H - 4.0;
-/// A slider row's control: the band plus a readout beside it. Wider than the
-/// toggle column, and NOT reserved on the other rows — the slider row has no
-/// chord, so it borrows the chord column rather than pushing every chord in
-/// the list left by half the plate.
+/// How far in from the row's right end a slider row's BAND begins. It runs
+/// from there out to the CHORD column's right edge, so it ends exactly where
+/// every other row's key binding ends and the toggle column stays clear —
+/// a band that stopped short of the chords read as a control someone had
+/// forgotten to finish. Not reserved on the other rows: the slider row has
+/// no chord, so it borrows the chord column rather than pushing every chord
+/// in the list left by half the plate.
pub const SLIDER_W: f32 = 180.0;
-/// The readout's width and its gap from the band. The readout is drawn by
+/// The readout's width and its gap from the band. It sits to the LEFT of the
+/// band, because the band's right end is spoken for. The readout is drawn by
/// the dialog, not by the toolkit slider's own: the dialog claims its rect as
/// a text occluder, and the clamp lets through only text carrying the
/// dialog's exact bounds (see `Dialog::popover`), so the stamp's readout
@@ -508,21 +512,33 @@ impl Dialog {
self.slider_drag
}
- /// Where a slider row draws its control: the row's right end.
- fn slider_rect(r: Rect) -> Rect {
- Rect { x: r.x + r.width - 8.0 - SLIDER_W, y: r.y + 2.0, width: SLIDER_W, height: ROW_H - 4.0 }
+ /// The switch column's width: reserved on EVERY row as soon as any row
+ /// has a toggle, so the chord column keeps a straight edge.
+ fn toggle_col(&self) -> f32 {
+ if self.rows.iter().any(|r| r.toggle.is_some()) { TOGGLE_W + 12.0 } else { 0.0 }
}
- /// The band inside that control — the stamp is painted over exactly
- /// this, so the pointer maps to the value where the band is drawn.
- fn slider_band_rect(r: Rect) -> Rect {
- let s = Self::slider_rect(r);
- Rect { width: (s.width - READOUT_W - READOUT_GAP).max(10.0), ..s }
+ /// The band a slider row draws — the stamp is painted over exactly this,
+ /// so the pointer maps to the value where the band is drawn. It ends at
+ /// the chord column's right edge, not the row's, which is why it needs
+ /// the roster rather than the rect alone.
+ fn slider_band_rect(&self, r: Rect) -> Rect {
+ let x = r.x + r.width - 8.0 - SLIDER_W;
+ let right = r.x + r.width - 8.0 - self.toggle_col();
+ Rect { x, y: r.y + 2.0, width: (right - x).max(10.0), height: ROW_H - 4.0 }
+ }
+
+ /// The whole control: the band plus the readout lane ahead of it. This is
+ /// what the pointer tests against, so the wheel turns the slider over the
+ /// readout too.
+ fn slider_rect(&self, r: Rect) -> Rect {
+ let b = self.slider_band_rect(r);
+ Rect { x: b.x - READOUT_W - READOUT_GAP, width: b.width + READOUT_W + READOUT_GAP, ..b }
}
/// The band's (x, width), captured at a press for the drag.
- fn slider_track(r: Rect) -> (f32, f32) {
- let b = Self::slider_band_rect(r);
+ fn slider_track_of(&self, r: Rect) -> (f32, f32) {
+ let b = self.slider_band_rect(r);
(b.x, b.width)
}
@@ -743,7 +759,7 @@ impl Paint for Dialog {
// the reservation the chords step left on toggle rows and the column
// reads as ragged, which is worse than the strip of air it costs.
let list = list_rect(rect);
- let toggle_col = if self.rows.iter().any(|r| r.toggle.is_some()) { TOGGLE_W + 12.0 } else { 0.0 };
+ let toggle_col = self.toggle_col();
if self.rows.is_empty() {
let ty = cce_ui::layout::align_text_y(list.y, ROW_H, font_size, 0.0);
let empty = match self.mode {
@@ -765,7 +781,11 @@ impl Paint for Dialog {
// state could not be read. On the plate it reads like the rest.
// A slider row's control is wider than the toggle column and
// takes the chord column's place on that one row.
- let ctl_col = if row.slider.is_some() { SLIDER_W + 12.0 } else { toggle_col };
+ let ctl_col = if row.slider.is_some() {
+ (r.x + r.width) - self.slider_rect(r).x + 4.0
+ } else {
+ toggle_col
+ };
let hl = Rect { width: (r.width - ctl_col).max(0.0), ..r };
if i == self.selected {
ctx.rounded_rect(hl, ctrl_r, (true, true, true, true), [accent[0], accent[1], accent[2], 0.16]);
@@ -826,15 +846,15 @@ impl Paint for Dialog {
Paint::paint(&*self.toggle_stamps[on as usize], tr, ctx);
}
if let Some(v) = row.slider {
- let s = Self::slider_rect(r);
- Paint::paint(&*self.slider_stamp, Self::slider_band_rect(r), ctx);
- // The readout, right-aligned in its lane after the band —
+ let band = self.slider_band_rect(r);
+ Paint::paint(&*self.slider_stamp, band, ctx);
+ // The readout, right-aligned in its lane ahead of the band —
// the dialog's own text, so it clears the occlusion clamp.
let readout = format!("{}%", v.round() as i64);
let rw = display::measure_text_width(&readout, &family, font_size);
ctx.text_with(
readout,
- s.x + s.width - rw,
+ band.x - READOUT_GAP - rw,
ty,
font_size,
label_color,
@@ -925,11 +945,15 @@ impl Input for Dialog {
self.selected = i;
if self.rows[i].slider.is_some() {
// On the band: take hold and jump there. On the
- // rest of the row: selected, and nothing to run.
+ // rest of the row — the readout lane included:
+ // selected, and nothing to run. A press tests the
+ // BAND rather than the whole control, or a click
+ // on the readout would jump the value to the end
+ // of the range nearest it.
if let Some(r) = self.row_rect(rect, i) {
- let s = Self::slider_rect(r);
+ let s = self.slider_band_rect(r);
if *x >= s.x && *x < s.x + s.width {
- self.slider_track = Self::slider_track(r);
+ self.slider_track = self.slider_track_of(r);
self.slider_drag = true;
self.slide_to(*x);
}
@@ -979,7 +1003,7 @@ impl Input for Dialog {
if let Some(i) = self.row_at(rect, *x, *y) {
if self.rows[i].slider.is_some() {
if let Some(r) = self.row_rect(rect, i) {
- let s = Self::slider_rect(r);
+ let s = self.slider_rect(r);
if *x >= s.x && *x < s.x + s.width {
return self.scroll_slider(delta.notches_y());
}
diff --git a/src/main.rs b/src/main.rs
index 903dcc6..e394540 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -8432,12 +8432,13 @@ mod tests {
d.set_occluding(false);
// The first row's rect, as the widget lays it out: the list starts
- // below the strip and the query line; the control sits at the row's
- // right end, the band ahead of the readout lane.
+ // below the strip and the query line; the band begins SLIDER_W in
+ // from the row's right end and runs out to the chord column's right
+ // edge — with no toggle row in this list, that is the row's own.
let list_y = 12.0 + 30.0 + 8.0 + 30.0 + 8.0;
let row_y = list_y + 12.0;
let band_x = 520.0 - 12.0 - 8.0 - SLIDER_W;
- let band_w = SLIDER_W - 60.0 - 8.0;
+ let band_w = SLIDER_W;
// Press at three quarters along the band: the value lands three
// quarters into the range, and the row is not activated as a pick.
@@ -8481,6 +8482,53 @@ mod tests {
assert_eq!(d.take_slider_change(), None, "over the label the wheel is the list's");
}
+ /// The band ends where the key bindings do. The chord column's right edge
+ /// steps left by the switch column as soon as any row carries a toggle,
+ /// and the band follows it — so the control lines up with the chords
+ /// beneath it instead of running on past them into the switches.
+ #[test]
+ fn dialog_slider_band_ends_at_the_chord_column() {
+ use crate::dialog::{Dialog, Row, SLIDER_W, TOGGLE_W};
+ use cce_ui::widget::{ElementState, MouseButton, WidgetHost};
+ let mut ctx = cce_ui::context::UiContext::new();
+ let mut d = Dialog::new();
+ d.set_visible(true);
+ WidgetHost::set_rect(&mut d, 0.0, 0.0, 520.0, 420.0);
+ let (id, ptr) = (d.id(), d.as_ptr_mut());
+ ctx.register_widget(id, ptr);
+ d.set_rows(vec![
+ Row { id: "zoom_level".into(), label: "Zoom".into(), chord: String::new(), swatch: None, toggle: None, slider: Some(100.0) },
+ Row { id: "show_grid".into(), label: "Show Grid".into(), chord: "Ctrl+G".into(), swatch: None, toggle: Some(true), slider: None },
+ ]);
+ d.set_slider_range(20.0, 320.0);
+ d.set_page(10);
+ d.set_occluding(false);
+
+ let row_y = 12.0 + 30.0 + 8.0 + 30.0 + 8.0 + 12.0;
+ let row_right = 520.0 - 12.0 - 8.0;
+ let band_right = row_right - (TOGGLE_W + 12.0);
+ assert!(band_right < row_right, "the switch column pulls the band in");
+
+ // The band's last pixel is the range's top; the switch column past it
+ // is not the band's.
+ assert!(d.mouse_input(MouseButton::Left, ElementState::Pressed, band_right - 1.0, row_y, &mut ctx));
+ assert!(d.slider_dragging(), "the band reaches the chord column's edge");
+ let v = d.take_slider_change().expect("a press on the band reports a value");
+ assert!((v - 320.0).abs() < 4.0, "the band's end is the range's end, got {v}");
+ d.mouse_input(MouseButton::Left, ElementState::Released, band_right - 1.0, row_y, &mut ctx);
+
+ d.mouse_input(MouseButton::Left, ElementState::Pressed, band_right + 4.0, row_y, &mut ctx);
+ assert!(!d.slider_dragging(), "past the chord column the row is not the band");
+ assert_eq!(d.take_slider_change(), None);
+ d.mouse_input(MouseButton::Left, ElementState::Released, band_right + 4.0, row_y, &mut ctx);
+
+ // The readout lane sits ahead of the band and takes no hold either.
+ let lane_x = row_right - SLIDER_W - 60.0 - 8.0;
+ d.mouse_input(MouseButton::Left, ElementState::Pressed, lane_x + 4.0, row_y, &mut ctx);
+ assert!(!d.slider_dragging(), "the readout is a readout, not a track");
+ assert_eq!(d.take_slider_change(), None);
+ }
+
/// A right press is the dialog's while it is open: inside the plate it is
/// swallowed — no context menu opens for the pane beneath, which used to
/// come up over the modal with its labels clipped — and outside it