1pub mod layout;
4pub mod node;
5pub mod reconcile;
6
7pub(crate) use self::layout::measure_pan_view;
8pub use self::node::PanViewNode;
9
10use std::sync::Arc;
11
12use crate::callback::Callback;
13use crate::core::element::{Element, ElementKind, Key};
14use crate::core::event::{KeyCode, KeyEvent};
15use crate::style::Length;
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19pub struct PanMetrics {
20 pub content_w: u16,
22 pub content_h: u16,
24 pub viewport_w: u16,
26 pub viewport_h: u16,
28 pub max_x: i32,
30 pub max_y: i32,
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
36pub struct PanEvent {
37 pub x: i32,
39 pub y: i32,
41 pub metrics: PanMetrics,
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
47pub struct PanKeymap(u8);
48
49impl PanKeymap {
50 pub const NONE: Self = Self(0);
52 pub const ARROWS: Self = Self(1 << 0);
54 pub const VIM: Self = Self(1 << 1);
56 pub const DEFAULT: Self = Self(Self::ARROWS.0 | Self::VIM.0);
58
59 pub fn contains(self, other: Self) -> bool {
61 (self.0 & other.0) == other.0
62 }
63}
64
65impl std::ops::BitOr for PanKeymap {
66 type Output = Self;
67
68 fn bitor(self, rhs: Self) -> Self {
69 Self(self.0 | rhs.0)
70 }
71}
72
73impl std::ops::BitOrAssign for PanKeymap {
74 fn bitor_assign(&mut self, rhs: Self) {
75 self.0 |= rhs.0;
76 }
77}
78
79impl std::ops::BitAnd for PanKeymap {
80 type Output = Self;
81
82 fn bitand(self, rhs: Self) -> Self {
83 Self(self.0 & rhs.0)
84 }
85}
86
87impl std::ops::BitAndAssign for PanKeymap {
88 fn bitand_assign(&mut self, rhs: Self) {
89 self.0 &= rhs.0;
90 }
91}
92
93impl std::ops::Not for PanKeymap {
94 type Output = Self;
95
96 fn not(self) -> Self {
97 Self(!self.0)
98 }
99}
100
101impl Default for PanKeymap {
102 fn default() -> Self {
103 Self::DEFAULT
104 }
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
108pub(crate) enum PanAction {
109 Delta(i16, i16),
110}
111
112fn signed_step(step: u16) -> i16 {
113 step.min(i16::MAX as u16) as i16
114}
115
116pub(crate) fn pan_action_from_key(
117 key: &KeyEvent,
118 keymap: PanKeymap,
119 key_step: (u16, u16),
120) -> Option<PanAction> {
121 let x = signed_step(key_step.0);
122 let y = signed_step(key_step.1);
123 match key.code {
124 KeyCode::Left if keymap.contains(PanKeymap::ARROWS) => Some(PanAction::Delta(-x, 0)),
125 KeyCode::Right if keymap.contains(PanKeymap::ARROWS) => Some(PanAction::Delta(x, 0)),
126 KeyCode::Up if keymap.contains(PanKeymap::ARROWS) => Some(PanAction::Delta(0, -y)),
127 KeyCode::Down if keymap.contains(PanKeymap::ARROWS) => Some(PanAction::Delta(0, y)),
128 KeyCode::Char('h') if keymap.contains(PanKeymap::VIM) => Some(PanAction::Delta(-x, 0)),
129 KeyCode::Char('l') if keymap.contains(PanKeymap::VIM) => Some(PanAction::Delta(x, 0)),
130 KeyCode::Char('k') if keymap.contains(PanKeymap::VIM) => Some(PanAction::Delta(0, -y)),
131 KeyCode::Char('j') if keymap.contains(PanKeymap::VIM) => Some(PanAction::Delta(0, y)),
132 _ => None,
133 }
134}
135
136pub(crate) fn pan_metrics(
137 content_w: u16,
138 content_h: u16,
139 viewport_w: u16,
140 viewport_h: u16,
141) -> PanMetrics {
142 PanMetrics {
143 content_w,
144 content_h,
145 viewport_w,
146 viewport_h,
147 max_x: i32::from(content_w.saturating_sub(viewport_w)),
148 max_y: i32::from(content_h.saturating_sub(viewport_h)),
149 }
150}
151
152pub(crate) fn clamp_pan_offset((x, y): (i32, i32), metrics: PanMetrics, clamp: bool) -> (i32, i32) {
153 if clamp {
154 (x.clamp(0, metrics.max_x), y.clamp(0, metrics.max_y))
155 } else {
156 (x, y)
157 }
158}
159
160fn free_axis_bounds(content: u16, viewport: u16, margin: u16) -> (i32, i32) {
161 let margin = margin.min(content.max(1)).min(viewport.max(1));
162 (
163 -(i32::from(viewport) - i32::from(margin)),
164 i32::from(content) - i32::from(margin),
165 )
166}
167
168pub(crate) fn bound_pan_offset(
169 (x, y): (i32, i32),
170 metrics: PanMetrics,
171 clamp: bool,
172 free_pan_margin: Option<(u16, u16)>,
173) -> (i32, i32) {
174 if clamp {
175 return clamp_pan_offset((x, y), metrics, true);
176 }
177
178 let Some((margin_x, margin_y)) = free_pan_margin else {
179 return (x, y);
180 };
181
182 let (min_x, max_x) = free_axis_bounds(metrics.content_w, metrics.viewport_w, margin_x);
183 let (min_y, max_y) = free_axis_bounds(metrics.content_h, metrics.viewport_h, margin_y);
184 (x.clamp(min_x, max_x), y.clamp(min_y, max_y))
185}
186
187pub(crate) fn apply_pan_delta(
188 (x, y): (i32, i32),
189 dx: i16,
190 dy: i16,
191 metrics: PanMetrics,
192 clamp: bool,
193 free_pan_margin: Option<(u16, u16)>,
194) -> (i32, i32) {
195 let next_x = x.saturating_add(i32::from(dx));
196 let next_y = y.saturating_add(i32::from(dy));
197 bound_pan_offset((next_x, next_y), metrics, clamp, free_pan_margin)
198}
199
200pub(crate) fn apply_pan_action(
201 offset: (i32, i32),
202 action: PanAction,
203 metrics: PanMetrics,
204 clamp: bool,
205 free_pan_margin: Option<(u16, u16)>,
206) -> (i32, i32) {
207 match action {
208 PanAction::Delta(dx, dy) => {
209 apply_pan_delta(offset, dx, dy, metrics, clamp, free_pan_margin)
210 }
211 }
212}
213
214#[derive(Clone)]
216pub struct PanView {
217 pub(crate) width: Length,
218 pub(crate) height: Length,
219 pub(crate) offset: Option<(i32, i32)>,
220 pub(crate) on_pan: Option<Callback<PanEvent>>,
221 pub(crate) clamp: bool,
222 pub(crate) center_content: bool,
223 pub(crate) free_pan_margin: Option<(u16, u16)>,
224 pub(crate) drag_to_pan: bool,
225 pub(crate) keymap: PanKeymap,
226 pub(crate) key_step: (u16, u16),
227 pub(crate) focusable: bool,
228 pub(crate) pan_state_key: Option<Key>,
229 pub(crate) child: Option<Box<Element>>,
230}
231
232impl Default for PanView {
233 fn default() -> Self {
234 Self {
235 width: Length::Flex(1),
236 height: Length::Flex(1),
237 offset: None,
238 on_pan: None,
239 clamp: true,
240 center_content: false,
241 free_pan_margin: None,
242 drag_to_pan: true,
243 keymap: PanKeymap::default(),
244 key_step: (4, 2),
245 focusable: true,
246 pan_state_key: None,
247 child: None,
248 }
249 }
250}
251
252impl PanView {
253 pub fn new() -> Self {
255 Self::default()
256 }
257
258 pub fn child(mut self, child: impl Into<Element>) -> Self {
260 self.child = Some(Box::new(child.into()));
261 self
262 }
263
264 pub fn width(mut self, width: Length) -> Self {
266 self.width = width;
267 self
268 }
269
270 pub fn height(mut self, height: Length) -> Self {
272 self.height = height;
273 self
274 }
275
276 pub fn offset(mut self, offset: (i32, i32)) -> Self {
278 self.offset = Some(offset);
279 self
280 }
281
282 pub fn on_pan(mut self, cb: Callback<PanEvent>) -> Self {
284 self.on_pan = Some(cb);
285 self
286 }
287
288 pub fn clamp(mut self, clamp: bool) -> Self {
290 self.clamp = clamp;
291 self
292 }
293
294 pub fn center_content(mut self, center: bool) -> Self {
296 self.center_content = center;
297 self
298 }
299
300 pub fn free_pan_margin(mut self, margin: u16) -> Self {
305 self.free_pan_margin = Some((margin, margin));
306 self
307 }
308
309 pub fn free_pan_margins(mut self, margins: (u16, u16)) -> Self {
311 self.free_pan_margin = Some(margins);
312 self
313 }
314
315 pub fn drag_to_pan(mut self, enabled: bool) -> Self {
317 self.drag_to_pan = enabled;
318 self
319 }
320
321 pub fn keymap(mut self, keymap: PanKeymap) -> Self {
323 self.keymap = keymap;
324 if keymap != PanKeymap::NONE {
325 self.focusable = true;
326 }
327 self
328 }
329
330 pub fn pan_keys(self, keymap: PanKeymap) -> Self {
332 self.keymap(keymap)
333 }
334
335 pub fn key_step(mut self, step: (u16, u16)) -> Self {
339 self.key_step = (step.0.max(1), step.1.max(1));
340 self
341 }
342
343 pub fn focusable(mut self, focusable: bool) -> Self {
345 self.focusable = focusable;
346 self
347 }
348
349 pub fn pan_state_key(mut self, key: impl Into<Key>) -> Self {
351 self.pan_state_key = Some(key.into());
352 self
353 }
354
355 pub fn state_key(self, key: impl Into<Arc<str>>) -> Self {
357 self.pan_state_key(Key::from(key.into()))
358 }
359}
360
361impl From<PanView> for Element {
362 fn from(value: PanView) -> Self {
363 Element::new(ElementKind::PanView(value))
364 }
365}
366
367impl crate::layout::hash::LayoutHash for PanView {
368 fn layout_hash(
369 &self,
370 hasher: &mut impl std::hash::Hasher,
371 recurse: &dyn Fn(&Element) -> Option<u64>,
372 ) -> Option<()> {
373 use std::hash::Hash;
374
375 self.width.hash(hasher);
376 self.height.hash(hasher);
377 self.clamp.hash(hasher);
378 self.center_content.hash(hasher);
379 self.free_pan_margin.hash(hasher);
380 self.drag_to_pan.hash(hasher);
381 self.keymap.hash(hasher);
382 self.key_step.hash(hasher);
383 self.focusable.hash(hasher);
384 if let Some(child) = &self.child {
385 recurse(child)?.hash(hasher);
386 }
387 Some(())
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394 use crate::core::node::{NodeKind, NodeTree};
395 use crate::layout::LayoutEngine;
396 use crate::style::Rect;
397 use crate::widgets::Text;
398
399 fn wide_tall_child() -> Element {
400 Text::new("0123456789\n0123456789\n0123456789").into()
401 }
402
403 fn small_child() -> Element {
404 Text::new("tiny").into()
405 }
406
407 #[test]
408 fn pan_view_clamps_offsets_to_content_bounds() {
409 let metrics = pan_metrics(20, 10, 8, 4);
410
411 assert_eq!(clamp_pan_offset((99, 99), metrics, true), (12, 6));
412 assert_eq!(clamp_pan_offset((99, 99), metrics, false), (99, 99));
413 assert_eq!(clamp_pan_offset((-4, -2), metrics, false), (-4, -2));
414 }
415
416 #[test]
417 fn pan_view_drag_direction_decreases_offset_when_dragging_right_down() {
418 let metrics = pan_metrics(20, 10, 8, 4);
419
420 assert_eq!(apply_pan_delta((5, 3), -2, -1, metrics, true, None), (3, 2));
421 assert_eq!(apply_pan_delta((5, 3), 2, 1, metrics, true, None), (7, 4));
422 assert_eq!(
423 apply_pan_delta((0, 0), -2, -1, metrics, false, None),
424 (-2, -1)
425 );
426 }
427
428 #[test]
429 fn pan_view_free_pan_margin_bounds_unclamped_offsets() {
430 let metrics = pan_metrics(20, 10, 8, 4);
431
432 assert_eq!(
433 bound_pan_offset((-99, -99), metrics, false, Some((1, 1))),
434 (-7, -3)
435 );
436 assert_eq!(
437 bound_pan_offset((99, 99), metrics, false, Some((1, 1))),
438 (19, 9)
439 );
440 }
441
442 #[test]
443 fn pan_view_keymap_matches_arrows_and_vim() {
444 let key = |code| KeyEvent {
445 code,
446 mods: crate::core::event::KeyMods::default(),
447 };
448
449 assert_eq!(
450 pan_action_from_key(&key(KeyCode::Right), PanKeymap::ARROWS, (4, 2)),
451 Some(PanAction::Delta(4, 0))
452 );
453 assert_eq!(
454 pan_action_from_key(&key(KeyCode::Char('h')), PanKeymap::VIM, (4, 2)),
455 Some(PanAction::Delta(-4, 0))
456 );
457 assert_eq!(
458 pan_action_from_key(&key(KeyCode::Char('h')), PanKeymap::ARROWS, (4, 2)),
459 None
460 );
461 assert_eq!(
462 pan_action_from_key(&key(KeyCode::Char('u')), PanKeymap::VIM, (4, 2)),
463 None
464 );
465 }
466
467 #[test]
468 fn pan_view_reconcile_clamps_and_offsets_child_rect() {
469 let root: Element = PanView::new()
470 .width(Length::Px(5))
471 .height(Length::Px(2))
472 .offset((99, 99))
473 .child(wide_tall_child())
474 .into();
475 let mut tree = NodeTree::new();
476 LayoutEngine::reconcile_with_focus(
477 &mut tree,
478 &root,
479 Rect {
480 x: 0,
481 y: 0,
482 w: 5,
483 h: 2,
484 },
485 None,
486 );
487
488 let node = tree.node(tree.root);
489 let NodeKind::PanView(pan) = &node.kind else {
490 panic!("expected PanView root");
491 };
492 assert_eq!((pan.content_w, pan.content_h), (10, 3));
493 assert_eq!((pan.viewport_w, pan.viewport_h), (5, 2));
494 assert_eq!((pan.offset_x, pan.offset_y), (5, 1));
495
496 let child = tree.node(node.children[0]);
497 assert_eq!(child.rect.x, -5);
498 assert_eq!(child.rect.y, -1);
499 assert_eq!(child.rect.w, 10);
500 assert_eq!(child.rect.h, 3);
501 }
502
503 #[test]
504 fn pan_view_keyboard_updates_uncontrolled_offset_and_persists_key() {
505 use std::cell::RefCell;
506 use std::rc::Rc;
507
508 let events = Rc::new(RefCell::new(Vec::new()));
509 let events_cb = events.clone();
510 let root: Element = PanView::new()
511 .width(Length::Px(5))
512 .height(Length::Px(2))
513 .pan_state_key("pan-test")
514 .on_pan(Callback::new(move |event| {
515 events_cb.borrow_mut().push(event);
516 }))
517 .child(wide_tall_child())
518 .into();
519 let mut tree = NodeTree::new();
520 let bounds = Rect {
521 x: 0,
522 y: 0,
523 w: 5,
524 h: 2,
525 };
526 LayoutEngine::reconcile_with_focus(&mut tree, &root, bounds, None);
527
528 let root_id = tree.root;
529 let handled = crate::app::input::handlers::pan_view::handle_key(
530 &mut tree,
531 root_id,
532 &KeyEvent {
533 code: KeyCode::Right,
534 mods: crate::core::event::KeyMods::default(),
535 },
536 );
537 assert!(handled);
538 let NodeKind::PanView(pan) = &tree.node(tree.root).kind else {
539 panic!("expected PanView root");
540 };
541 assert_eq!((pan.offset_x, pan.offset_y), (4, 0));
542 assert_eq!(events.borrow()[0].x, 4);
543
544 LayoutEngine::reconcile_with_focus(&mut tree, &root, bounds, None);
545 let NodeKind::PanView(pan) = &tree.node(tree.root).kind else {
546 panic!("expected PanView root");
547 };
548 assert_eq!((pan.offset_x, pan.offset_y), (4, 0));
549 }
550
551 #[test]
552 fn pan_view_can_center_smaller_content_with_negative_offset() {
553 let root: Element = PanView::new()
554 .width(Length::Px(20))
555 .height(Length::Px(10))
556 .clamp(false)
557 .center_content(true)
558 .child(small_child())
559 .into();
560 let mut tree = NodeTree::new();
561 LayoutEngine::reconcile_with_focus(
562 &mut tree,
563 &root,
564 Rect {
565 x: 0,
566 y: 0,
567 w: 20,
568 h: 10,
569 },
570 None,
571 );
572
573 let node = tree.node(tree.root);
574 let NodeKind::PanView(pan) = &node.kind else {
575 panic!("expected PanView root");
576 };
577 assert_eq!((pan.offset_x, pan.offset_y), (-8, -4));
578 let child = tree.node(node.children[0]);
579 assert_eq!(child.rect.x, 8);
580 assert_eq!(child.rect.y, 4);
581 }
582}