system settings
git clone https://git.lucas.co/cce-system-interface.git
refactor: cce-system-settings chrome + audio drags on routed events (6bd)
Chrome (input_handler): search box, page dropdown, and the page-button loop
dispatch one Event through ui_context.propagate_event each, same coords and
short-circuits; take_click/take_change drains stay.
Audio page: the sink/source drag hooks and their app-held drag indices are
gone — presses were already routed through the section roots, and the
router's drag-target machinery now delivers DragUpdate/DragEnd to the
sliders. Values surface through the existing take_change drain, which now
also syncs the paired spinbox (as the old drag path did) and runs on pointer
moves so drag volume actions fire live. The page pointer hooks stay for the
other pages' app-owned ScrollRegions. dispatch_page_event's out-of-bounds
gate exempts router drags, so a drag that leaves the page viewport keeps
updating (the old hook path had no viewport gate).
Verified: 18 tests; audio render stream byte-identical; live on the new
build: page-dropdown popover -> Fonts -> back (the converted chrome path),
slider track press arms without a jump (Slider never jump-to-clicked —
baseline identical). PRE-EXISTING, baseline-reproduced: audio slider WHEEL
is dead (scroll_enabled is set; the event dies earlier in page dispatch) —
same deferred class as the 6u inner-wheel items. Held slider drag is not
headlessly drivable — user spot-check.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018u7qTwzX95dd5ysAkaSCLk
src/input_handler.rs | 43 ++++++++++++++++++++-------
src/pages/audio.rs | 82 +++++++---------------------------------------------
2 files changed, 44 insertions(+), 81 deletions(-)
diff --git a/src/input_handler.rs b/src/input_handler.rs
index 817e354..357694c 100644
--- a/src/input_handler.rs
+++ b/src/input_handler.rs
@@ -14,8 +14,10 @@ impl SystemInterface {
let sh_logical = self.height as f32 / s;
if self.search_open && ly_no_scroll >= (sh_logical - 42.0) {
- let changed = self.search_box.cursor_moved(lx_no_scroll, ly_no_scroll, &mut self.ui_context);
- if changed {
+ // Routed (6bd): chrome coords, no scroll offset — same as the direct call.
+ let ev = cce_ui::widget::Event::PointerMove { x: lx_no_scroll, y: ly_no_scroll, local_x: lx_no_scroll, local_y: ly_no_scroll };
+ let sb = self.search_box.as_ptr_mut();
+ if self.ui_context.propagate_event(&ev, sb) {
self.needs_rebuild = true;
}
return true;
@@ -33,8 +35,12 @@ impl SystemInterface {
let ly = self.cursor_y / s + self.scroll_y;
cce_ui::widget::hover_animation::set_cursor_pos(lx, ly_no_scroll);
let mut changed = false;
- if self.page_dropdown.cursor_moved(lx_no_scroll, ly_no_scroll, &mut self.ui_context) {
- changed = true;
+ {
+ let ev = cce_ui::widget::Event::PointerMove { x: lx_no_scroll, y: ly_no_scroll, local_x: lx_no_scroll, local_y: ly_no_scroll };
+ let dd = self.page_dropdown.as_ptr_mut();
+ if self.ui_context.propagate_event(&ev, dd) {
+ changed = true;
+ }
}
// Drag updates are high-priority overrides
@@ -53,6 +59,16 @@ impl SystemInterface {
if self.dispatch_page_event(&event) {
changed = true;
}
+ // Routed drags (6bd): a DragUpdate delivered inside the dispatch surfaces as
+ // widget take_change — drain and act per move, as the old page drag hooks did.
+ let mut move_actions = Vec::new();
+ self.propagate_widget_changes(&mut move_actions);
+ if !move_actions.is_empty() {
+ changed = true;
+ for action in move_actions {
+ self.handle_action(&action);
+ }
+ }
}
let phys_x = x;
@@ -80,8 +96,9 @@ impl SystemInterface {
let sh_logical = self.height as f32 / s;
if self.search_open && ly_no_scroll >= (sh_logical - 42.0) {
- let handled = self.search_box.mouse_input(button, state, lx_no_scroll, ly_no_scroll, &mut self.ui_context);
- if handled {
+ let ev = cce_ui::widget::Event::MouseButton { button, state, x: lx_no_scroll, y: ly_no_scroll, local_x: lx_no_scroll, local_y: ly_no_scroll };
+ let sb = self.search_box.as_ptr_mut();
+ if self.ui_context.propagate_event(&ev, sb) {
self.needs_rebuild = true;
}
return true;
@@ -109,7 +126,9 @@ impl SystemInterface {
}
}
- if self.page_dropdown.mouse_input(button, state, lx_no_scroll, ly_no_scroll, &mut self.ui_context) {
+ let dd_ev = cce_ui::widget::Event::MouseButton { button, state, x: lx_no_scroll, y: ly_no_scroll, local_x: lx_no_scroll, local_y: ly_no_scroll };
+ let dd_ptr = self.page_dropdown.as_ptr_mut();
+ if self.ui_context.propagate_event(&dd_ev, dd_ptr) {
if self.page_dropdown.take_change() {
let idx = self.page_dropdown.selected;
if idx < Page::ALL.len() {
@@ -133,10 +152,14 @@ impl SystemInterface {
let mut button_handled = false;
let mut clicked_action = None;
- for (btn, action) in &mut self.page_buttons[self.scrollable_buttons_start_idx..] {
- if btn.mouse_input(button, state, phys_x, phys_y, &mut self.ui_context) {
+ let btn_ev = cce_ui::widget::Event::MouseButton { button, state, x: phys_x, y: phys_y, local_x: phys_x, local_y: phys_y };
+ let btn_ptrs: Vec<_> = self.page_buttons[self.scrollable_buttons_start_idx..].iter_mut().map(|(b, _)| b.as_ptr_mut()).collect();
+ for ptr in btn_ptrs {
+ if self.ui_context.propagate_event(&btn_ev, ptr) {
button_handled = true;
}
+ }
+ for (btn, action) in &mut self.page_buttons[self.scrollable_buttons_start_idx..] {
if btn.take_click() {
clicked_action = Some(action.clone());
button_handled = true;
@@ -279,7 +302,7 @@ impl SystemInterface {
event,
Event::PointerMove { .. } | Event::MouseButton { .. } | Event::MouseWheel { .. }
);
- if is_pointer_event && !self.page_scroll_bar.dragging {
+ if is_pointer_event && !self.page_scroll_bar.dragging && !self.ui_context.is_dragging {
if let Event::PointerMove { x, y, .. }
| Event::MouseButton { x, y, .. }
| Event::MouseWheel { x, y, .. } = event
diff --git a/src/pages/audio.rs b/src/pages/audio.rs
index a12a120..1795cd2 100644
--- a/src/pages/audio.rs
+++ b/src/pages/audio.rs
@@ -29,8 +29,6 @@ pub struct AudioState {
pub source_spinboxes: Vec<Box<cce_ui::widget::Adapted<cce_ui::widget::Spinbox>>>,
pub sink_sliders: Vec<Box<cce_ui::widget::Adapted<cce_ui::widget::Slider>>>,
pub source_sliders: Vec<Box<cce_ui::widget::Adapted<cce_ui::widget::Slider>>>,
- pub sink_dragging: Option<usize>,
- pub source_dragging: Option<usize>,
}
#[derive(Debug, Clone)]
@@ -130,8 +128,6 @@ pub async fn fetch_audio_state() -> AudioState {
source_spinboxes: Vec::new(),
sink_sliders: Vec::new(),
source_sliders: Vec::new(),
- sink_dragging: None,
- source_dragging: None,
}
}
@@ -466,82 +462,28 @@ impl crate::pages::AppPage for AudioState {
for (i, slider) in self.sink_sliders.iter_mut().enumerate() {
if slider.take_change() {
let id = self.sinks[i].id;
+ // Keep the paired spinbox display in step, as the old drag path did.
+ if let Some(sb) = self.sink_spinboxes.get_mut(i) {
+ sb.value = slider.value();
+ }
actions.push(AppAction::Audio(AudioMessage::SinkVolume(id, slider.value() as f32 / 100.0)));
}
}
for (i, slider) in self.source_sliders.iter_mut().enumerate() {
if slider.take_change() {
let id = self.sources[i].id;
- actions.push(AppAction::Audio(AudioMessage::SourceVolume(id, slider.value() as f32 / 100.0)));
- }
- }
- }
-
- fn handle_pointer_move(
- &mut self,
- lx: f32,
- ly: f32,
- actions: &mut Vec<crate::app::AppAction>,
- _ctx: &mut cce_ui::context::UiContext,
- ) -> bool {
- if let Some(idx) = self.sink_dragging {
- if let Some(slider) = self.sink_sliders.get_mut(idx) {
- if slider.drag_update(lx, ly) {
- let id = self.sinks[idx].id;
- let val = slider.inner().value();
- if idx < self.sink_spinboxes.len() {
- self.sink_spinboxes[idx].value = (val * 100.0).round() as i32;
- }
- actions.push(AppAction::Audio(AudioMessage::SinkVolume(id, val)));
- return true;
- }
- }
- } else if let Some(idx) = self.source_dragging {
- if let Some(slider) = self.source_sliders.get_mut(idx) {
- if slider.drag_update(lx, ly) {
- let id = self.sources[idx].id;
- let val = slider.inner().value();
- if idx < self.source_spinboxes.len() {
- self.source_spinboxes[idx].value = (val * 100.0).round() as i32;
- }
- actions.push(AppAction::Audio(AudioMessage::SourceVolume(id, val)));
- return true;
+ if let Some(sb) = self.source_spinboxes.get_mut(i) {
+ sb.value = slider.value();
}
+ actions.push(AppAction::Audio(AudioMessage::SourceVolume(id, slider.value() as f32 / 100.0)));
}
}
- false
- }
-
- fn handle_pointer_down(&mut self, _lx: f32, _ly: f32, _ctx: &mut cce_ui::context::UiContext) -> bool {
- for (i, s) in self.sink_sliders.iter().enumerate() {
- if s.is_dragging() {
- self.sink_dragging = Some(i);
- return true;
- }
- }
- for (i, s) in self.source_sliders.iter().enumerate() {
- if s.is_dragging() {
- self.source_dragging = Some(i);
- return true;
- }
- }
- false
}
- fn handle_pointer_up(&mut self, _ctx: &mut cce_ui::context::UiContext) -> bool {
- let mut any = false;
- if let Some(idx) = self.sink_dragging {
- self.sink_sliders[idx].drag_end();
- self.sink_dragging = None;
- any = true;
- }
- if let Some(idx) = self.source_dragging {
- self.source_sliders[idx].drag_end();
- self.source_dragging = None;
- any = true;
- }
- any
- }
+ // The pointer drag hooks are GONE (6bd routed events): slider drags ride the
+ // router's drag-target machinery — presses were already routed through the section
+ // roots, and DragUpdate/DragEnd now reach the sliders the same way. Value changes
+ // surface through the take_change drain above.
}
#[cfg(test)]
@@ -575,8 +517,6 @@ mod tests {
Box::new(Slider::new()),
],
source_sliders: vec![],
- sink_dragging: None,
- source_dragging: None,
};
let mut layout = AdaptiveGrid::new(260.0, 20.0);
let pc = view(&mut state, 10.0, 20.0, 800.0, 600.0, &[false, false], &mut layout, &mut cce_ui::context::UiContext::new());