From 59063153e14899d3bc1a1e44d952f49042050927 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 22 Feb 2023 12:13:31 +0000 Subject: [PATCH 1/6] Doc: theme config --- crates/kas-core/src/theme/config.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/kas-core/src/theme/config.rs b/crates/kas-core/src/theme/config.rs index f2c5c3b27..0ffe9cc4d 100644 --- a/crates/kas-core/src/theme/config.rs +++ b/crates/kas-core/src/theme/config.rs @@ -33,6 +33,25 @@ pub struct Config { color_schemes: BTreeMap, /// Font aliases, used when searching for a font family matching the key. + /// + /// Example: + /// ```yaml + /// font_aliases: + /// sans-serif: + /// mode: Prepend + /// list: + /// - noto sans + /// ``` + /// + /// Fonts are named by *family*. Several standard families exist, e.g. + /// "serif", "sans-serif", "monospace"; these resolve to a list + /// of aliases (e.g. "Noto Sans", "DejaVu Sans", "Arial"), each of which may + /// have further aliases. + /// + /// In the above example, "noto sans" is inserted at the top of the alias + /// list for "sans-serif". + /// + /// Supported modes: `Prepend`, `Append`, `Replace`. #[cfg_attr(feature = "serde", serde(default))] font_aliases: BTreeMap, From df51a680d8ac5902ffb7aeb9595aaff5a9cbef59 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Fri, 24 Feb 2023 15:47:48 +0000 Subject: [PATCH 2/6] ScrollComponent: stop momentum scrolling given another scroll action --- crates/kas-core/src/event/components.rs | 45 ++++++++++++++++--------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/crates/kas-core/src/event/components.rs b/crates/kas-core/src/event/components.rs index 5a57709b7..241befff1 100644 --- a/crates/kas-core/src/event/components.rs +++ b/crates/kas-core/src/event/components.rs @@ -86,6 +86,12 @@ impl Glide { None } } + + fn stop(&mut self) { + if matches!(self, Glide::Glide(_, _, _)) { + *self = Glide::None; + } + } } /// Logic for a scroll region @@ -145,11 +151,15 @@ impl ScrollComponent { /// The offset is clamped to the available scroll range. /// Returns [`Action::empty()`] if the offset is identical to the old offset, /// or [`Action::REGION_MOVED`] if the offset changes. + /// + /// Also cancels any momentum scrolling, but only if `offset` is not equal + /// to the current offset. pub fn set_offset(&mut self, offset: Offset) -> Action { - let offset = offset.min(self.max_offset).max(Offset::ZERO); + let offset = offset.clamp(Offset::ZERO, self.max_offset); if offset == self.offset { Action::empty() } else { + self.glide.stop(); self.offset = offset; Action::REGION_MOVED } @@ -165,6 +175,7 @@ impl ScrollComponent { /// may be set via [`EventMgr::set_scroll`] /// - returned `Action`: action to pass to the event manager pub fn focus_rect(&mut self, rect: Rect, window_rect: Rect) -> (Rect, Action) { + self.glide.stop(); let v = rect.pos - window_rect.pos; let off = Offset::conv(rect.size) - Offset::conv(window_rect.size); let offset = self.offset.max(v + off).min(v); @@ -193,15 +204,23 @@ impl ScrollComponent { } fn scroll_by_delta(&mut self, mgr: &mut EventMgr, d: Offset) -> bool { - let old_offset = self.offset; - *mgr |= self.set_offset(old_offset - d); - let delta = d - (old_offset - self.offset); + let mut delta = d; + let mut moved = false; + let offset = (self.offset - d).clamp(Offset::ZERO, self.max_offset); + if offset != self.offset { + moved = true; + delta = d - (self.offset - offset); + self.offset = offset; + *mgr |= Action::REGION_MOVED; + } + mgr.set_scroll(if delta != Offset::ZERO { Scroll::Offset(delta) } else { Scroll::Scrolled }); - old_offset != self.offset + + moved } /// Use an event to scroll, if possible @@ -219,7 +238,8 @@ impl ScrollComponent { /// `PressMove` is used to scroll by the motion delta and to track speed; /// `PressEnd` initiates momentum-scrolling if the speed is high enough. /// - /// Returns `(moved, response)`. + /// Returns `(moved, response)` where `moved` means *this component + /// scrolled* (scrolling of a parent is possible even if `!moved`). pub fn scroll_by_event( &mut self, mgr: &mut EventMgr, @@ -262,6 +282,7 @@ impl ScrollComponent { LineDelta(x, y) => mgr.config().scroll_distance((x, y)), PixelDelta(d) => d, }; + self.glide.stop(); moved = self.scroll_by_delta(mgr, delta); } Event::PressStart { source, coord, .. } @@ -283,15 +304,9 @@ impl ScrollComponent { // Momentum/glide scrolling: update per arbitrary step time until movment stops. let decay = mgr.config().scroll_flick_decay(); if let Some(delta) = self.glide.step(decay) { - let action = self.set_offset(self.offset - delta); - if !action.is_empty() { - *mgr |= action; - moved = true; - } - if delta == Offset::ZERO || !action.is_empty() { - // Note: when FPS > pixels/sec, delta may be zero while - // still scrolling. Glide returns None when we're done, - // but we're also done if unable to scroll further. + moved = self.scroll_by_delta(mgr, delta); + + if matches!(&self.glide, Glide::Glide(_, _, _)) { let dur = Duration::from_millis(GLIDE_POLL_MS); mgr.request_update(id, PAYLOAD_GLIDE, dur, true); mgr.set_scroll(Scroll::Scrolled); From 2cc0addb6f6a51d61e8393dfb4d9ab68569add6a Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Fri, 24 Feb 2023 17:07:29 +0000 Subject: [PATCH 3/6] Allow compound momentum scrolling --- crates/kas-core/src/event/components.rs | 179 +++++++++++++----------- 1 file changed, 101 insertions(+), 78 deletions(-) diff --git a/crates/kas-core/src/event/components.rs b/crates/kas-core/src/event/components.rs index 241befff1..ff49704d5 100644 --- a/crates/kas-core/src/event/components.rs +++ b/crates/kas-core/src/event/components.rs @@ -17,104 +17,118 @@ use std::time::{Duration, Instant}; const PAYLOAD_SELECT: u64 = 1 << 60; const PAYLOAD_GLIDE: u64 = (1 << 60) + 1; const GLIDE_POLL_MS: u64 = 3; +const GLIDE_MAX_SAMPLES: usize = 8; -#[derive(Clone, Debug, Default, PartialEq)] -enum Glide { - #[default] - None, - Drag(u8, [(Instant, Offset); 4]), - Glide(Instant, Vec2, Vec2), +#[derive(Clone, Debug)] +struct Glide { + samples: [(Instant, Offset); GLIDE_MAX_SAMPLES], + last: u32, + pressed: bool, + t_step: Instant, + vel: Vec2, + rest: Vec2, } -impl Glide { - fn move_delta(&mut self, delta: Offset) { - match self { - Glide::Drag(next, samples) => { - samples[*next as usize] = (Instant::now(), delta); - *next = (*next + 1) % 4; - } - _ => { - let x = (Instant::now(), delta); - *self = Glide::Drag(1, [x; 4]); - } +impl Default for Glide { + #[inline] + fn default() -> Self { + let now = Instant::now(); + + Glide { + samples: [(now, Offset::ZERO); GLIDE_MAX_SAMPLES], + last: 0, + pressed: false, + t_step: now, + vel: Vec2::ZERO, + rest: Vec2::ZERO, } } +} - fn opt_start(&mut self, timeout: Duration) -> bool { - if let Glide::Drag(_, samples) = self { - let now = Instant::now(); - let mut delta = Offset::ZERO; - let mut t0 = now; - for (time, d) in samples { - if *time + timeout >= now { - t0 = t0.min(*time); - delta += *d; - } +impl Glide { + fn press_start(&mut self) { + let next = (self.last as usize + 1) % GLIDE_MAX_SAMPLES; + self.samples[next] = (Instant::now(), Offset::ZERO); + self.last = next.cast(); + self.pressed = true; + } + + /// Returns true if component should immediately scroll by delta + fn press_move(&mut self, delta: Offset) -> bool { + let next = (self.last as usize + 1) % GLIDE_MAX_SAMPLES; + self.samples[next] = (Instant::now(), delta); + self.last = next.cast(); + self.vel == Vec2::ZERO + } + + /// Returns true if momentum scrolling starts + fn press_end(&mut self, timeout: Duration, pan_dist_thresh: f32) -> bool { + self.pressed = false; + + let now = Instant::now(); + let mut delta = Offset::ZERO; + let mut t0 = now; + for (time, d) in &self.samples { + if *time + timeout >= now { + t0 = t0.min(*time); + delta += *d; } - let dur = now - t0; - let v = Vec2::conv(delta) / dur.as_secs_f32(); - if dur >= Duration::from_millis(1) && v != Vec2::ZERO { - *self = Glide::Glide(now, v, Vec2::ZERO); - true - } else { - *self = Glide::None; - false + } + let dur = timeout; //now - t0; + let mut is_start = false; + if f32::conv(delta.distance_l_inf()) >= pan_dist_thresh { + if self.vel == Vec2::ZERO { + self.t_step = Instant::now(); + is_start = true; } + self.vel += Vec2::conv(delta) / dur.as_secs_f32(); } else { - false + self.vel = Vec2::ZERO; + self.rest = Vec2::ZERO; } + is_start } - fn step(&mut self, (decay_mul, decay_sub): (f32, f32)) -> Option { - if let Glide::Glide(start, v, rest) = self { - let now = Instant::now(); - let dur = (now - *start).as_secs_f32(); - let d = *v * dur + *rest; - let delta = Offset::conv_approx(d); - let rest = d - Vec2::conv(delta); - - if v.abs().max_comp() >= 1.0 { - let mut v = *v * decay_mul.powf(dur); - v = v - v.abs().min(Vec2::splat(decay_sub * dur)) * v.sign(); - *self = Glide::Glide(now, v, rest); - Some(delta) - } else { - *self = Glide::None; - None - } - } else { - None + fn step(&mut self, timeout: Duration, (decay_mul, decay_sub): (f32, f32)) -> Option { + // Stop on click+hold as well as min velocity. Do not stop on reaching + // the maximum scroll offset since we might still be scrolling a parent! + let stop = self.pressed && self.samples[self.last as usize].0.elapsed() > timeout; + if stop || self.vel.abs().max_comp() < 1.0 { + self.vel = Vec2::ZERO; + self.rest = Vec2::ZERO; + return None; } + + let now = Instant::now(); + let dur = (now - self.t_step).as_secs_f32(); + self.t_step = now; + + let v = self.vel * decay_mul.powf(dur); + self.vel = v - v.abs().min(Vec2::splat(decay_sub * dur)) * v.sign(); + + let d = self.vel * dur + self.rest; + let delta = Offset::conv_trunc(d); + self.rest = d - Vec2::conv(delta); + + Some(delta) } fn stop(&mut self) { - if matches!(self, Glide::Glide(_, _, _)) { - *self = Glide::None; - } + self.vel = Vec2::ZERO; + self.rest = Vec2::ZERO; } } /// Logic for a scroll region /// /// This struct handles some scroll logic. It does not provide scroll bars. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, Default)] pub struct ScrollComponent { max_offset: Offset, offset: Offset, glide: Glide, } -impl Default for ScrollComponent { - #[inline] - fn default() -> Self { - ScrollComponent { - max_offset: Offset::ZERO, - offset: Offset::ZERO, - glide: Glide::None, - } - } -} - impl ScrollComponent { /// Get the maximum offset /// @@ -290,23 +304,28 @@ impl ScrollComponent { { let icon = Some(CursorIcon::Grabbing); mgr.grab_press_unique(id, source, coord, icon); + self.glide.press_start(); } Event::PressMove { delta, .. } => { - self.glide.move_delta(delta); - moved = self.scroll_by_delta(mgr, delta); + if self.glide.press_move(delta) { + moved = self.scroll_by_delta(mgr, delta); + } } Event::PressEnd { .. } => { - if self.glide.opt_start(mgr.config().scroll_flick_timeout()) { + let timeout = mgr.config().scroll_flick_timeout(); + let pan_dist_thresh = mgr.config().pan_dist_thresh(); + if self.glide.press_end(timeout, pan_dist_thresh) { mgr.request_update(id, PAYLOAD_GLIDE, Duration::new(0, 0), true); } } Event::TimerUpdate(pl) if pl == PAYLOAD_GLIDE => { // Momentum/glide scrolling: update per arbitrary step time until movment stops. + let timeout = mgr.config().scroll_flick_timeout(); let decay = mgr.config().scroll_flick_decay(); - if let Some(delta) = self.glide.step(decay) { + if let Some(delta) = self.glide.step(timeout, decay) { moved = self.scroll_by_delta(mgr, delta); - if matches!(&self.glide, Glide::Glide(_, _, _)) { + if self.glide.vel != Vec2::ZERO { let dur = Duration::from_millis(GLIDE_POLL_MS); mgr.request_update(id, PAYLOAD_GLIDE, dur, true); mgr.set_scroll(Scroll::Scrolled); @@ -392,6 +411,7 @@ impl TextInput { ), }; mgr.grab_press_unique(w_id, source, coord, icon); + self.glide.press_start(); action } Event::PressMove { @@ -400,7 +420,7 @@ impl TextInput { delta, .. } => { - self.glide.move_delta(delta); + self.glide.press_move(delta); match source { PressSource::Touch(touch_id) => match self.touch_phase { TouchPhase::Start(id, start_coord) if id == touch_id => { @@ -422,7 +442,9 @@ impl TextInput { } } Event::PressEnd { source, .. } => { - if self.glide.opt_start(mgr.config().scroll_flick_timeout()) + let timeout = mgr.config().scroll_flick_timeout(); + let pan_dist_thresh = mgr.config().pan_dist_thresh(); + if self.glide.press_end(timeout, pan_dist_thresh) && (matches!(source, PressSource::Touch(id) if self.touch_phase == TouchPhase::Pan(id)) || matches!(source, PressSource::Mouse(..) if mgr.config_enable_mouse_text_pan())) { @@ -445,8 +467,9 @@ impl TextInput { } Event::TimerUpdate(pl) if pl == PAYLOAD_GLIDE => { // Momentum/glide scrolling: update per arbitrary step time until movment stops. + let timeout = mgr.config().scroll_flick_timeout(); let decay = mgr.config().scroll_flick_decay(); - if let Some(delta) = self.glide.step(decay) { + if let Some(delta) = self.glide.step(timeout, decay) { let dur = Duration::from_millis(GLIDE_POLL_MS); mgr.request_update(w_id, PAYLOAD_GLIDE, dur, true); Action::Pan(delta) From 847e778efff4f0ca371445166b56fdedd907ff69 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Fri, 24 Feb 2023 17:12:17 +0000 Subject: [PATCH 4/6] Fixes --- crates/kas-core/src/event/components.rs | 3 ++- crates/kas-view/src/list_view.rs | 2 +- crates/kas-view/src/matrix_view.rs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/kas-core/src/event/components.rs b/crates/kas-core/src/event/components.rs index ff49704d5..ac49e01ad 100644 --- a/crates/kas-core/src/event/components.rs +++ b/crates/kas-core/src/event/components.rs @@ -156,7 +156,8 @@ impl ScrollComponent { /// change in offset. In practice the caller will likely be performing all /// required updates regardless and the return value can be safely ignored. pub fn set_sizes(&mut self, window_size: Size, content_size: Size) -> Action { - self.max_offset = Offset::conv(content_size) - Offset::conv(window_size); + self.max_offset = + (Offset::conv(content_size) - Offset::conv(window_size)).max(Offset::ZERO); self.set_offset(self.offset) } diff --git a/crates/kas-view/src/list_view.rs b/crates/kas-view/src/list_view.rs index 2509f21de..466ce771a 100644 --- a/crates/kas-view/src/list_view.rs +++ b/crates/kas-view/src/list_view.rs @@ -675,7 +675,7 @@ impl_scope! { Event::PressStart { source, coord, .. } => { return if source.is_primary() { mgr.grab_press_unique(self.id(), source, coord, None); - self.press_phase = PressPhase::Pan; + self.press_phase = PressPhase::Start(coord); Response::Used } else { Response::Unused diff --git a/crates/kas-view/src/matrix_view.rs b/crates/kas-view/src/matrix_view.rs index bfc241e8b..8ea0f3dc2 100644 --- a/crates/kas-view/src/matrix_view.rs +++ b/crates/kas-view/src/matrix_view.rs @@ -681,7 +681,7 @@ impl_scope! { Event::PressStart { source, coord, .. } => { return if source.is_primary() { mgr.grab_press_unique(self.id(), source, coord, None); - self.press_phase = PressPhase::Pan; + self.press_phase = PressPhase::Start(coord); Response::Used } else { Response::Unused From 213790bf6fc2844d0969c49759e2fea92314ba59 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Sat, 25 Feb 2023 13:42:47 +0000 Subject: [PATCH 5/6] List/MatrixView: better selection/drag-scroll interaction --- crates/kas-core/src/event/components.rs | 14 ++- crates/kas-view/src/lib.rs | 7 -- crates/kas-view/src/list_view.rs | 114 +++++++++--------------- crates/kas-view/src/matrix_view.rs | 114 +++++++++--------------- 4 files changed, 100 insertions(+), 149 deletions(-) diff --git a/crates/kas-core/src/event/components.rs b/crates/kas-core/src/event/components.rs index ac49e01ad..c48837abc 100644 --- a/crates/kas-core/src/event/components.rs +++ b/crates/kas-core/src/event/components.rs @@ -130,6 +130,12 @@ pub struct ScrollComponent { } impl ScrollComponent { + /// True if momentum scrolling is active + #[inline] + pub fn is_gliding(&self) -> bool { + self.glide.vel != Vec2::ZERO + } + /// Get the maximum offset /// /// Note: the minimum offset is always zero. @@ -307,12 +313,16 @@ impl ScrollComponent { mgr.grab_press_unique(id, source, coord, icon); self.glide.press_start(); } - Event::PressMove { delta, .. } => { + Event::PressMove { source, delta, .. } + if self.max_offset != Offset::ZERO && mgr.config_enable_pan(source) => + { if self.glide.press_move(delta) { moved = self.scroll_by_delta(mgr, delta); } } - Event::PressEnd { .. } => { + Event::PressEnd { source, .. } + if self.max_offset != Offset::ZERO && mgr.config_enable_pan(source) => + { let timeout = mgr.config().scroll_flick_timeout(); let pan_dist_thresh = mgr.config().pan_dist_thresh(); if self.glide.press_end(timeout, pan_dist_thresh) { diff --git a/crates/kas-view/src/lib.rs b/crates/kas-view/src/lib.rs index 55d77dc95..2db56b210 100644 --- a/crates/kas-view/src/lib.rs +++ b/crates/kas-view/src/lib.rs @@ -56,13 +56,6 @@ pub enum SelectionMsg { Deselect(K), } -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -enum PressPhase { - None, - Start(kas::geom::Coord), - Pan, -} - /// Selection mode used by [`ListView`] #[derive(Clone, Copy, Debug, Default)] pub enum SelectionMode { diff --git a/crates/kas-view/src/list_view.rs b/crates/kas-view/src/list_view.rs index 466ce771a..379d84fca 100644 --- a/crates/kas-view/src/list_view.rs +++ b/crates/kas-view/src/list_view.rs @@ -5,9 +5,9 @@ //! List view controller -use super::{driver, Driver, PressPhase, SelectionError, SelectionMode, SelectionMsg}; +use super::{driver, Driver, SelectionError, SelectionMode, SelectionMsg}; use kas::event::components::ScrollComponent; -use kas::event::{Command, CursorIcon, Scroll}; +use kas::event::{Command, Scroll}; use kas::layout::{solve_size_rules, AlignHints}; #[allow(unused)] use kas::model::SharedData; use kas::model::{ListData, SharedDataMut}; @@ -74,8 +74,7 @@ impl_scope! { sel_mode: SelectionMode, // TODO(opt): replace selection list with RangeOrSet type? selection: LinearSet, - press_phase: PressPhase, - press_target: Option, + press_target: Option<(usize, T::Key)>, } impl Self @@ -141,7 +140,6 @@ impl_scope! { scroll: Default::default(), sel_mode: SelectionMode::None, selection: Default::default(), - press_phase: PressPhase::None, press_target: None, } } @@ -662,7 +660,7 @@ impl_scope! { } fn handle_event(&mut self, mgr: &mut EventMgr, event: Event) -> Response { - match event { + let response = match event { Event::Update { .. } => { let data_ver = self.data.version(); if data_ver > self.data_ver { @@ -672,52 +670,6 @@ impl_scope! { } return Response::Used; } - Event::PressStart { source, coord, .. } => { - return if source.is_primary() { - mgr.grab_press_unique(self.id(), source, coord, None); - self.press_phase = PressPhase::Start(coord); - Response::Used - } else { - Response::Unused - }; - } - Event::PressMove { coord, .. } => { - if let PressPhase::Start(start_coord) = self.press_phase { - if mgr.config_test_pan_thresh(coord - start_coord) { - self.press_phase = PressPhase::Pan; - } - } - match self.press_phase { - PressPhase::Pan => { - mgr.update_grab_cursor(self.id(), CursorIcon::Grabbing); - // fall through to scroll handler - } - _ => return Response::Used, - } - } - Event::PressEnd { ref end_id, .. } => { - if self.press_phase == PressPhase::Pan { - // fall through to scroll handler - } else if end_id.is_some() { - if let Some(ref key) = self.press_target { - if mgr.config().mouse_nav_focus() { - for w in &mut self.widgets { - if w.key.as_ref().map(|k| k == key).unwrap_or(false) { - mgr.next_nav_focus(&mut w.widget, false, false); - break; - } - } - } - - if !matches!(self.sel_mode, SelectionMode::None) { - mgr.push(SelectMsg); - } - } - return Response::Used; - } else { - return Response::Used; - } - } Event::Command(cmd) => { let last = self.data.len().wrapping_sub(1); if last == usize::MAX { @@ -761,34 +713,53 @@ impl_scope! { Response::Unused }; } - _ => (), // fall through to scroll handler - } + Event::PressStart { source, coord, .. } if source.is_primary() && mgr.config().mouse_nav_focus() => { + if let Some((index, ref key)) = self.press_target { + let w = &mut self.widgets[index]; + if w.key.as_ref().map(|k| k == key).unwrap_or(false) { + mgr.next_nav_focus(&mut w.widget, false, false); + } + } - let (moved, r) = self + // Press may also be grabbed by scroll component (replacing + // this). Either way we can select on PressEnd. + mgr.grab_press_unique(self.id(), source, coord, None); + Response::Used + } + Event::PressMove { .. } => Response::Used, + Event::PressEnd { source, coord, .. } if source.is_primary() => { + if let Some((index, ref key)) = self.press_target { + let w = &mut self.widgets[index]; + if !matches!(self.sel_mode, SelectionMode::None) + && !self.scroll.is_gliding() + && w.key.as_ref().map(|k| k == key).unwrap_or(false) + && w.widget.rect().contains(coord + self.scroll.offset()) + { + mgr.push(SelectMsg); + } + } + Response::Used + } + _ => Response::Unused, // fall through to scroll handler + }; + + let (moved, sber_response) = self .scroll .scroll_by_event(mgr, event, self.id(), self.core.rect); if moved { mgr.config_mgr(|mgr| self.update_widgets(mgr)); } - r + response | sber_response } fn handle_unused(&mut self, mgr: &mut EventMgr, event: Event) -> Response { - if let Event::PressStart { source, coord, .. } = event { - if source.is_primary() { - // We request a grab with our ID, hence the - // PressMove/PressEnd events are matched in handle_event(). - mgr.grab_press_unique(self.id(), source, coord, None); - self.press_phase = PressPhase::Start(coord); - let index = mgr.last_child().unwrap(); - self.press_target = self.widgets[index].key.clone(); - Response::Used - } else { - Response::Unused + if matches!(&event, Event::PressStart { .. }) { + if let Some(index) = mgr.last_child() { + self.press_target = self.widgets[index].key.clone().map(|k| (index, k)); } - } else { - self.handle_event(mgr, event) } + + self.handle_event(mgr, event) } fn handle_message(&mut self, mgr: &mut EventMgr) { @@ -803,7 +774,10 @@ impl_scope! { self.driver.on_message(mgr, &mut w.widget, &self.data, &key); } else { // Message is from self - key = self.press_target.clone().unwrap(); + key = match self.press_target.clone() { + Some((_, k)) => k, + None => return, + }; } if let Some(SelectMsg) = mgr.try_pop() { diff --git a/crates/kas-view/src/matrix_view.rs b/crates/kas-view/src/matrix_view.rs index 8ea0f3dc2..f10190ef9 100644 --- a/crates/kas-view/src/matrix_view.rs +++ b/crates/kas-view/src/matrix_view.rs @@ -5,9 +5,9 @@ //! Matrix view controller -use super::{driver, Driver, PressPhase, SelectionError, SelectionMode, SelectionMsg}; +use super::{driver, Driver, SelectionError, SelectionMode, SelectionMsg}; use kas::event::components::ScrollComponent; -use kas::event::{Command, CursorIcon, Scroll}; +use kas::event::{Command, Scroll}; use kas::layout::{solve_size_rules, AlignHints}; #[allow(unused)] use kas::model::SharedData; use kas::model::{MatrixData, SharedDataMut}; @@ -80,8 +80,7 @@ impl_scope! { sel_mode: SelectionMode, // TODO(opt): replace selection list with RangeOrSet type? selection: LinearSet, - press_phase: PressPhase, - press_target: Option, + press_target: Option<(usize, T::Key)>, } impl Self @@ -117,7 +116,6 @@ impl_scope! { scroll: Default::default(), sel_mode: SelectionMode::None, selection: Default::default(), - press_phase: PressPhase::None, press_target: None, } } @@ -669,7 +667,7 @@ impl_scope! { } fn handle_event(&mut self, mgr: &mut EventMgr, event: Event) -> Response { - match event { + let response = match event { Event::Update { .. } => { let data_ver = self.data.version(); if data_ver > self.data_ver { @@ -678,52 +676,6 @@ impl_scope! { } return Response::Used; } - Event::PressStart { source, coord, .. } => { - return if source.is_primary() { - mgr.grab_press_unique(self.id(), source, coord, None); - self.press_phase = PressPhase::Start(coord); - Response::Used - } else { - Response::Unused - }; - } - Event::PressMove { coord, .. } => { - if let PressPhase::Start(start_coord) = self.press_phase { - if mgr.config_test_pan_thresh(coord - start_coord) { - self.press_phase = PressPhase::Pan; - } - } - match self.press_phase { - PressPhase::Pan => { - mgr.update_grab_cursor(self.id(), CursorIcon::Grabbing); - // fall through to scroll handler - } - _ => return Response::Used, - } - } - Event::PressEnd { ref end_id, .. } => { - if self.press_phase == PressPhase::Pan { - // fall through to scroll handler - } else if end_id.is_some() { - if let Some(ref key) = self.press_target { - if mgr.config().mouse_nav_focus() { - for w in &mut self.widgets { - if w.key.as_ref().map(|k| k == key).unwrap_or(false) { - mgr.next_nav_focus(&mut w.widget, false, false); - break; - } - } - } - - if !matches!(self.sel_mode, SelectionMode::None) { - mgr.push(SelectMsg); - } - } - return Response::Used; - } else { - return Response::Used; - } - } Event::Command(cmd) => { if self.data.is_empty() { return Response::Unused; @@ -790,34 +742,53 @@ impl_scope! { Response::Unused }; } - _ => (), // fall through to scroll handler - } + Event::PressStart { source, coord, .. } if source.is_primary() && mgr.config().mouse_nav_focus() => { + if let Some((index, ref key)) = self.press_target { + let w = &mut self.widgets[index]; + if w.key.as_ref().map(|k| k == key).unwrap_or(false) { + mgr.next_nav_focus(&mut w.widget, false, false); + } + } + + // Press may also be grabbed by scroll component (replacing + // this). Either way we can select on PressEnd. + mgr.grab_press_unique(self.id(), source, coord, None); + Response::Used + } + Event::PressMove { .. } => Response::Used, + Event::PressEnd { source, coord, .. } if source.is_primary() => { + if let Some((index, ref key)) = self.press_target { + let w = &mut self.widgets[index]; + if !matches!(self.sel_mode, SelectionMode::None) + && !self.scroll.is_gliding() + && w.key.as_ref().map(|k| k == key).unwrap_or(false) + && w.widget.rect().contains(coord + self.scroll.offset()) + { + mgr.push(SelectMsg); + } + } + Response::Used + } + _ => Response::Unused, // fall through to scroll handler + }; - let (moved, r) = self + let (moved, sber_response) = self .scroll .scroll_by_event(mgr, event, self.id(), self.core.rect); if moved { mgr.config_mgr(|mgr| self.update_widgets(mgr)); } - r + response | sber_response } fn handle_unused(&mut self, mgr: &mut EventMgr, event: Event) -> Response { - if let Event::PressStart { source, coord, .. } = event { - if source.is_primary() { - // We request a grab with our ID, hence the - // PressMove/PressEnd events are matched in handle_event(). - mgr.grab_press_unique(self.id(), source, coord, None); - self.press_phase = PressPhase::Start(coord); - let index = mgr.last_child().unwrap(); - self.press_target = self.widgets[index].key.clone(); - Response::Used - } else { - Response::Unused + if matches!(&event, Event::PressStart { .. }) { + if let Some(index) = mgr.last_child() { + self.press_target = self.widgets[index].key.clone().map(|k| (index, k)); } - } else { - self.handle_event(mgr, event) } + + self.handle_event(mgr, event) } fn handle_message(&mut self, mgr: &mut EventMgr) { @@ -832,7 +803,10 @@ impl_scope! { self.driver.on_message(mgr, &mut w.widget, &self.data, &key); } else { // Message is from self - key = self.press_target.clone().unwrap(); + key = match self.press_target.clone() { + Some((_, k)) => k, + None => return, + }; } if let Some(SelectMsg) = mgr.try_pop() { From 6229840c3fed2b24f959fd2e28f3931c8e2411aa Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Sat, 25 Feb 2023 14:59:19 +0000 Subject: [PATCH 6/6] Add kas::theme::SelectionStyle --- crates/kas-core/src/theme/draw.rs | 22 ++++++------- crates/kas-core/src/theme/simple_theme.rs | 24 ++++++++++---- crates/kas-core/src/theme/style.rs | 22 +++++++++++++ crates/kas-macros/src/extends.rs | 6 ++-- crates/kas-view/src/list_view.rs | 39 +++++++++++++++++++++-- crates/kas-view/src/matrix_view.rs | 39 +++++++++++++++++++++-- 6 files changed, 124 insertions(+), 28 deletions(-) diff --git a/crates/kas-core/src/theme/draw.rs b/crates/kas-core/src/theme/draw.rs index 280ccb7b1..f42010c04 100644 --- a/crates/kas-core/src/theme/draw.rs +++ b/crates/kas-core/src/theme/draw.rs @@ -5,7 +5,7 @@ //! "Handle" types used by themes -use super::{FrameStyle, MarkStyle, SizeMgr, TextClass, ThemeSize}; +use super::{FrameStyle, MarkStyle, SelectionStyle, SizeMgr, TextClass, ThemeSize}; use crate::dir::Direction; use crate::draw::color::Rgb; use crate::draw::{Draw, DrawIface, DrawShared, DrawSharedImpl, ImageId, PassType}; @@ -194,13 +194,13 @@ impl<'a> DrawMgr<'a> { self.h.separator(rect); } - /// Draw a selection box + /// Draw a selection highlight / frame /// - /// This appears as a dashed box or similar around this `rect`. Note that - /// the selection indicator is drawn *outside* of this rect, within a margin - /// of size [`SizeMgr::inner_margins`] that is expected to be present around this box. - pub fn selection_box(&mut self, rect: Rect) { - self.h.selection_box(rect); + /// Adjusts the background color and/or draws a line around the given rect. + /// In the latter case, a margin of size [`SizeMgr::inner_margins`] around + /// `rect` is expected. + pub fn selection(&mut self, rect: Rect, style: SelectionStyle) { + self.h.selection(rect, style); } /// Draw text @@ -412,12 +412,8 @@ pub trait ThemeDraw { /// Draw a separator in the given `rect` fn separator(&mut self, rect: Rect); - /// Draw a selection box - /// - /// This appears as a dashed box or similar around this `rect`. Note that - /// the selection indicator is drawn *outside* of this rect, within a margin - /// of size `inner_margin` that is expected to be present around this box. - fn selection_box(&mut self, rect: Rect); + /// Draw a selection highlight / frame + fn selection(&mut self, rect: Rect, style: SelectionStyle); /// Draw text /// diff --git a/crates/kas-core/src/theme/simple_theme.rs b/crates/kas-core/src/theme/simple_theme.rs index 50f419dac..10763916d 100644 --- a/crates/kas-core/src/theme/simple_theme.rs +++ b/crates/kas-core/src/theme/simple_theme.rs @@ -20,7 +20,7 @@ use kas::text::{fonts, Effect, TextApi, TextDisplay}; use kas::theme::dimensions as dim; use kas::theme::{Background, FrameStyle, MarkStyle, TextClass}; use kas::theme::{ColorsLinear, Config, InputState, Theme}; -use kas::theme::{ThemeControl, ThemeDraw, ThemeSize}; +use kas::theme::{SelectionStyle, ThemeControl, ThemeDraw, ThemeSize}; use kas::{Action, WidgetId}; /// A simple theme @@ -362,12 +362,24 @@ where self.draw.rect(outer, self.cols.frame); } - fn selection_box(&mut self, rect: Rect) { + fn selection(&mut self, rect: Rect, style: SelectionStyle) { let inner = Quad::conv(rect); - let outer = inner.grow(self.w.dims.m_inner.into()); - // TODO: this should use its own colour and a stippled pattern - let col = self.cols.text_sel_bg; - self.draw.frame(outer, inner, col); + match style { + SelectionStyle::Highlight => { + self.draw.rect(inner, self.cols.text_sel_bg); + } + SelectionStyle::Frame => { + let outer = inner.grow(self.w.dims.m_inner.into()); + // TODO: this should use its own colour and a stippled pattern + let col = self.cols.accent; + self.draw.frame(outer, inner, col); + } + SelectionStyle::Both => { + let outer = inner.grow(self.w.dims.m_inner.into()); + self.draw.rect(outer, self.cols.accent); + self.draw.rect(inner, self.cols.text_sel_bg); + } + } } fn text(&mut self, id: &WidgetId, rect: Rect, text: &TextDisplay, _: TextClass) { diff --git a/crates/kas-core/src/theme/style.rs b/crates/kas-core/src/theme/style.rs index 57cf73a8d..a5d1b90cb 100644 --- a/crates/kas-core/src/theme/style.rs +++ b/crates/kas-core/src/theme/style.rs @@ -94,6 +94,28 @@ pub enum FrameStyle { Window, } +/// Selection style hint +/// +/// How to draw selections +#[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] +pub enum SelectionStyle { + /// Adjust background color + Highlight, + /// Draw a frame around the selection + Frame, + /// Both + Both, +} + +impl SelectionStyle { + /// True if an external margin is required + /// + /// Margin size: [`SizeMgr::inner_margins`] + pub fn is_external(self) -> bool { + matches!(self, SelectionStyle::Frame | SelectionStyle::Both) + } +} + /// Class of text drawn /// /// Themes choose font, font size, colour, and alignment based on this. diff --git a/crates/kas-macros/src/extends.rs b/crates/kas-macros/src/extends.rs index f0ad463d9..fb0361fce 100644 --- a/crates/kas-macros/src/extends.rs +++ b/crates/kas-macros/src/extends.rs @@ -64,7 +64,7 @@ impl Extends { (#base).get_clip_rect() } - fn frame(&mut self, id: &WidgetId, rect: Rect, style: FrameStyle, bg: Background) { + fn frame(&mut self, id: &WidgetId, rect: Rect, style: ::kas::theme::FrameStyle, bg: Background) { (#base).frame(id, rect, style, bg); } @@ -72,8 +72,8 @@ impl Extends { (#base).separator(rect); } - fn selection_box(&mut self, rect: Rect) { - (#base).selection_box(rect); + fn selection(&mut self, rect: Rect, style: ::kas::theme::SelectionStyle) { + (#base).selection(rect, style); } fn text(&mut self, id: &WidgetId, rect: Rect, text: &TextDisplay, class: TextClass) { diff --git a/crates/kas-view/src/list_view.rs b/crates/kas-view/src/list_view.rs index 379d84fca..1fb2d59fb 100644 --- a/crates/kas-view/src/list_view.rs +++ b/crates/kas-view/src/list_view.rs @@ -12,6 +12,7 @@ use kas::layout::{solve_size_rules, AlignHints}; #[allow(unused)] use kas::model::SharedData; use kas::model::{ListData, SharedDataMut}; use kas::prelude::*; +use kas::theme::SelectionStyle; #[allow(unused)] // doc links use kas_widgets::ScrollBars; use kas_widgets::SelectMsg; @@ -72,6 +73,7 @@ impl_scope! { child_size: Size, scroll: ScrollComponent, sel_mode: SelectionMode, + sel_style: SelectionStyle, // TODO(opt): replace selection list with RangeOrSet type? selection: LinearSet, press_target: Option<(usize, T::Key)>, @@ -139,6 +141,7 @@ impl_scope! { child_size: Size::ZERO, scroll: Default::default(), sel_mode: SelectionMode::None, + sel_style: SelectionStyle::Highlight, selection: Default::default(), press_target: None, } @@ -236,6 +239,32 @@ impl_scope! { self } + /// Get the current selection style + pub fn selection_style(&self) -> SelectionStyle { + self.sel_style + } + /// Set the current selection style + /// + /// By default, [`SelectionStyle::Highlight`] is used. Other modes may + /// add margin between elements. + pub fn set_selection_style(&mut self, style: SelectionStyle) -> Action { + let action = if style.is_external() != self.sel_style.is_external() { + Action::RESIZE + } else { + Action::empty() + }; + self.sel_style = style; + action + } + /// Set the selection style (inline) + /// + /// See [`Self::set_selection_style`] documentation. + #[must_use] + pub fn with_selection_style(mut self, style: SelectionStyle) -> Self { + self.sel_style = style; + self + } + /// Read the list of selected entries /// /// With mode [`SelectionMode::Single`] this may contain zero or one entry; @@ -473,7 +502,11 @@ impl_scope! { impl Layout for Self { fn size_rules(&mut self, size_mgr: SizeMgr, mut axis: AxisInfo) -> SizeRules { // We use an invisible frame for highlighting selections, drawing into the margin - let inner_margin = size_mgr.inner_margins().extract(axis); + let inner_margin = if self.sel_style.is_external() { + size_mgr.inner_margins().extract(axis) + } else { + (0, 0) + }; let frame = kas::layout::FrameRules::new(0, inner_margin, (0, 0)); let other = axis.other().map(|mut size| { @@ -579,12 +612,12 @@ impl_scope! { let offset = self.scroll_offset(); draw.with_clip_region(self.core.rect, offset, |mut draw| { for child in &mut self.widgets[..self.cur_len.cast()] { - draw.recurse(&mut child.widget); if let Some(ref key) = child.key { if self.selection.contains(key) { - draw.selection_box(child.widget.rect()); + draw.selection(child.widget.rect(), self.sel_style); } } + draw.recurse(&mut child.widget); } }); } diff --git a/crates/kas-view/src/matrix_view.rs b/crates/kas-view/src/matrix_view.rs index f10190ef9..8b21a6087 100644 --- a/crates/kas-view/src/matrix_view.rs +++ b/crates/kas-view/src/matrix_view.rs @@ -12,6 +12,7 @@ use kas::layout::{solve_size_rules, AlignHints}; #[allow(unused)] use kas::model::SharedData; use kas::model::{MatrixData, SharedDataMut}; use kas::prelude::*; +use kas::theme::SelectionStyle; #[allow(unused)] // doc links use kas_widgets::ScrollBars; use kas_widgets::SelectMsg; @@ -78,6 +79,7 @@ impl_scope! { child_size: Size, scroll: ScrollComponent, sel_mode: SelectionMode, + sel_style: SelectionStyle, // TODO(opt): replace selection list with RangeOrSet type? selection: LinearSet, press_target: Option<(usize, T::Key)>, @@ -115,6 +117,7 @@ impl_scope! { child_size: Size::ZERO, scroll: Default::default(), sel_mode: SelectionMode::None, + sel_style: SelectionStyle::Highlight, selection: Default::default(), press_target: None, } @@ -212,6 +215,32 @@ impl_scope! { self } + /// Get the current selection style + pub fn selection_style(&self) -> SelectionStyle { + self.sel_style + } + /// Set the current selection style + /// + /// By default, [`SelectionStyle::Highlight`] is used. Other modes may + /// add margin between elements. + pub fn set_selection_style(&mut self, style: SelectionStyle) -> Action { + let action = if style.is_external() != self.sel_style.is_external() { + Action::RESIZE + } else { + Action::empty() + }; + self.sel_style = style; + action + } + /// Set the selection style (inline) + /// + /// See [`Self::set_selection_style`] documentation. + #[must_use] + pub fn with_selection_style(mut self, style: SelectionStyle) -> Self { + self.sel_style = style; + self + } + /// Read the list of selected entries /// /// With mode [`SelectionMode::Single`] this may contain zero or one entry; @@ -451,7 +480,11 @@ impl_scope! { impl Layout for Self { fn size_rules(&mut self, size_mgr: SizeMgr, mut axis: AxisInfo) -> SizeRules { // We use an invisible frame for highlighting selections, drawing into the margin - let inner_margin = size_mgr.inner_margins().extract(axis); + let inner_margin = if self.sel_style.is_external() { + size_mgr.inner_margins().extract(axis) + } else { + (0, 0) + }; let frame = kas::layout::FrameRules::new(0, inner_margin, (0, 0)); let other = axis.other().map(|mut size| { @@ -569,10 +602,10 @@ impl_scope! { // visible, so check intersection before drawing: if rect.intersection(&child.widget.rect()).is_some() { if let Some(ref key) = child.key { - draw.recurse(&mut child.widget); if self.selection.contains(key) { - draw.selection_box(child.widget.rect()); + draw.selection(child.widget.rect(), self.sel_style); } + draw.recurse(&mut child.widget); } } }