calendar
git clone https://git.lucas.co/cce-calendar.git
Smooth the sidebar scroll; step months once per 48px of trackpad
The event list's `sidebar_scroll` is now driven by a ScrollMotion: a
notch is still one ROW_H, pixel deltas are 1:1, notches glide, a flick
coasts, and `tick_sidebar_scroll` from `Application::tick` carries the
drawn offset each frame. `select`'s reset to 0 is adopted by `reconcile`.
Over the grid the wheel keeps flipping months discretely, but a trackpad
gesture used to fire `shift_months` on every pixel event. `LineDelta`
steps once per notch as before; `PixelDelta` accumulates into
`month_wheel_px` and steps once per MONTH_STEP_PX (48), carrying the
remainder, and the finger lift (`ScrollPhase::FingerEnd`) clears it.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
src/main.rs | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++++------
1 file changed, 81 insertions(+), 8 deletions(-)
diff --git a/src/main.rs b/src/main.rs
index b698fb6..ca29c21 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -24,6 +24,7 @@ use wayland_client::QueueHandle;
use cce_ui::engine::{Application, EngineState, LogicalPosition, LogicalSize, WindowSettings};
use cce_ui::scene::layout::Rect;
use cce_ui::scene::paint::{AlignH, AlignV, DisplayList, PaintCtx, TextAttrs, TextLayout};
+use cce_ui::widget::scroll_motion::{current_scroll_phase, Bounds, ScrollMotion, ScrollPhase};
use cce_ui::widget::{ElementState, Key, KeyEvent, MouseButton, MouseScrollDelta, NamedKey};
const HEADER_H: f32 = 46.0;
@@ -32,6 +33,8 @@ const SIDEBAR_W: f32 = 300.0;
const PAD: f32 = 12.0;
const ROW_H: f32 = 36.0;
const INPUT_H: f32 = 40.0;
+/// Trackpad travel per month step over the grid (a wheel notch is one step).
+const MONTH_STEP_PX: f32 = 48.0;
const BG: [f32; 4] = [0.075, 0.08, 0.09, 1.0];
const BG_OTHER_MONTH: [f32; 4] = [0.06, 0.064, 0.072, 1.0];
@@ -190,6 +193,14 @@ struct CalendarApp {
today: NaiveDate,
win: (f32, f32),
sidebar_scroll: f32,
+ /// Drives `sidebar_scroll` (the drawn value) from the wheel: notches
+ /// glide, fingers track 1:1 and fling on the lift. `select` resets the
+ /// offset directly; the motion adopts that through `reconcile`.
+ sidebar_motion: ScrollMotion,
+ /// Trackpad pixels accumulated toward the next month step over the grid,
+ /// so a gesture flips one month per MONTH_STEP_PX rather than one per
+ /// pixel event. Cleared by a wheel notch and by the finger lift.
+ month_wheel_px: f32,
status: Option<String>,
}
@@ -246,6 +257,26 @@ impl CalendarApp {
}
}
+ /// How far the selected day's event rows overflow the sidebar's list area.
+ fn sidebar_overflow(&self, g: &Geom) -> f32 {
+ let events = self.events.get(&self.selected).map_or(0, Vec::len);
+ (events as f32 * ROW_H - (g.sidebar.height - 58.0 - INPUT_H - 8.0)).max(0.0)
+ }
+
+ /// Per-frame sidebar glide/coast; true while `sidebar_scroll` is still
+ /// moving, so the frame loop keeps drawing.
+ fn tick_sidebar_scroll(&mut self, dt: f32) -> bool {
+ self.sidebar_motion.reconcile(0.0, self.sidebar_scroll);
+ if !self.sidebar_motion.is_animating() {
+ return false;
+ }
+ let g = self.geom();
+ let overflow = self.sidebar_overflow(&g);
+ let moved = self.sidebar_motion.tick(dt, Bounds::max(0.0), Bounds::max(overflow));
+ self.sidebar_scroll = self.sidebar_motion.y.pos();
+ moved || self.sidebar_motion.is_animating()
+ }
+
fn shift_months(&mut self, delta: i32) {
let (y, m) = add_months(self.view.0, self.view.1, delta);
self.view = (y, m);
@@ -574,6 +605,8 @@ impl Application for CalendarApp {
today,
win: (1060.0, 720.0),
sidebar_scroll: 0.0,
+ sidebar_motion: ScrollMotion::new(),
+ month_wheel_px: 0.0,
status: None,
}
}
@@ -595,12 +628,15 @@ impl Application for CalendarApp {
}
}
- fn tick(&mut self, _dt: f32, needs_rebuild: &mut bool) {
+ fn tick(&mut self, dt: f32, needs_rebuild: &mut bool) {
let now = Local::now().date_naive();
if now != self.today {
self.today = now;
*needs_rebuild = true;
}
+ if self.tick_sidebar_scroll(dt) {
+ *needs_rebuild = true;
+ }
}
fn handle_resize(&mut self, width: f32, height: f32, _scale: f64) {
@@ -642,14 +678,51 @@ impl Application for CalendarApp {
fn handle_mouse_wheel(&mut self, delta: &MouseScrollDelta, pos: LogicalPosition, needs_rebuild: &mut bool) {
let g = self.geom();
if hit(&g.sidebar, pos.x, pos.y) {
- let events = self.events.get(&self.selected).map_or(0, Vec::len);
- let overflow = (events as f32 * ROW_H - (g.sidebar.height - 58.0 - INPUT_H - 8.0)).max(0.0);
- self.sidebar_scroll = (self.sidebar_scroll - delta.notches_y() * ROW_H).clamp(0.0, overflow);
- *needs_rebuild = true;
+ // A notch is one row, pixel deltas are 1:1. The motion glides
+ // notches and coasts a flick; `tick_sidebar_scroll` carries the
+ // drawn offset after it. A true return is the repaint signal.
+ let overflow = self.sidebar_overflow(&g);
+ self.sidebar_motion.reconcile(0.0, self.sidebar_scroll);
+ if self.sidebar_motion.apply(delta, (ROW_H, ROW_H), Bounds::max(0.0), Bounds::max(overflow)) {
+ self.sidebar_scroll = self.sidebar_motion.y.pos();
+ *needs_rebuild = true;
+ }
} else {
- let notches = delta.notches_y();
- if notches != 0.0 {
- self.shift_months(if notches < 0.0 { 1 } else { -1 });
+ // Month stepping stays discrete: one month per wheel notch. A
+ // trackpad gesture accumulates pixels and steps once per
+ // MONTH_STEP_PX instead of once per pixel event, carrying the
+ // remainder until the finger lifts.
+ let step = match delta {
+ MouseScrollDelta::LineDelta(_, y) => {
+ self.month_wheel_px = 0.0;
+ if *y < 0.0 {
+ 1
+ } else if *y > 0.0 {
+ -1
+ } else {
+ 0
+ }
+ }
+ MouseScrollDelta::PixelDelta(p) => {
+ if current_scroll_phase() == ScrollPhase::FingerEnd {
+ self.month_wheel_px = 0.0;
+ 0
+ } else {
+ self.month_wheel_px += p.y as f32;
+ if self.month_wheel_px <= -MONTH_STEP_PX {
+ self.month_wheel_px += MONTH_STEP_PX;
+ 1
+ } else if self.month_wheel_px >= MONTH_STEP_PX {
+ self.month_wheel_px -= MONTH_STEP_PX;
+ -1
+ } else {
+ 0
+ }
+ }
+ }
+ };
+ if step != 0 {
+ self.shift_months(step);
*needs_rebuild = true;
}
}