Skip to main content

flux_tui/components/
scrollviewer.rs

1use std::{
2	cell::RefCell,
3	rc::{Rc, Weak},
4};
5
6use super::*;
7use crate::canvas::GlyphArray;
8
9/// [Window] which is scrollable
10pub trait Scrollable: Window {
11	/// Can current [Scrollable] be scrolled horizontally / vertically
12	fn is_scrollable(&self, axis: Orientation) -> bool;
13
14	/// Scrolls page/line-wise (see [ScrollKind]) in the given direction
15	fn scroll(&mut self, direction: Direction, kind: ScrollKind);
16
17	/// Sets the horizontal / vertical offset
18	fn set_offset(&mut self, direction: Orientation, offset: usize);
19
20	/// Horizontal / Vertical offset of the scrolled content
21	fn offset(&self, axis: Orientation) -> usize;
22
23	/// Horizontal / Vertical size of the viewport for current [Scrollable]
24	fn viewport(&self, axis: Orientation) -> usize;
25
26	/// Should only be called by the [ScrollViewer] itself
27	fn set_scroll_viewer(&mut self, scroll_viewer: Weak<RefCell<ScrollViewer>>);
28}
29
30/// Base struct for storing different information required for the [Scrollable] trait
31/// Has all fns of [Scrollable] but doesn't implement the trait
32#[derive(Clone, Debug, Default)]
33pub struct ScrollBase {
34	/// Offset of the base 0,0 coordinate (as in rendering)
35	pub offset: Vector2D<usize>,
36	/// Viewport for the whole content to be rendered
37	pub viewport: Vector2D<usize>,
38	/// Actual size rendered
39	pub render_size: TPoint,
40	/// Weak reference to the owning scroll viewer
41	pub scroll_viewer: Weak<RefCell<ScrollViewer>>,
42}
43
44impl ScrollBase {
45	pub fn is_scrollable(&self, axis: Orientation) -> bool {
46		let idx = axis as usize;
47		self.scroll_viewer.strong_count() > usize::MIN
48			&& ((self.offset[idx] == usize::MIN
49				&& self.viewport[idx] > self.render_size[idx] as usize)
50				|| self.offset[idx] > usize::MIN)
51	}
52
53	pub fn scroll(&mut self, direction: Direction, kind: ScrollKind) {
54		let delta = match kind {
55			ScrollKind::Line => 1,
56			ScrollKind::Page => match direction {
57				Direction::Up | Direction::Down => self.render_size.y as usize,
58				Direction::Left | Direction::Right => self.render_size.x as usize,
59			},
60		};
61		match direction {
62			Direction::Up => {
63				if let Some(offset) = self.offset.y.checked_sub(delta) {
64					self.offset.y = offset;
65				}
66				else {
67					self.offset.y = usize::MIN;
68				}
69			}
70			Direction::Down => {
71				let offset = self.offset.y + delta;
72				if offset <= self.viewport.y - delta {
73					self.offset.y += delta;
74				}
75				else {
76					self.offset.y = self.viewport.y - delta;
77				}
78			}
79			Direction::Left => {
80				if let Some(offset) = self.offset.x.checked_sub(delta) {
81					self.offset.x -= offset;
82				}
83				else {
84					self.offset.x = usize::MIN
85				}
86			}
87			Direction::Right => {
88				let offset = self.offset.x + delta;
89				if offset <= self.viewport.x - delta {
90					self.offset.x += delta;
91				}
92				else {
93					self.offset.x = self.viewport.x - delta;
94				}
95			}
96		}
97	}
98
99	pub fn set_offset(&mut self, direction: Orientation, offset: usize) {
100		match direction {
101			Orientation::Horizontal => {
102				if offset < self.viewport.x {
103					self.offset.x = offset;
104				}
105				else {
106					self.offset.x = self.viewport.x.checked_sub(1).unwrap_or_default();
107				}
108			}
109			Orientation::Vertical => {
110				if offset < self.viewport.y {
111					self.offset.y = offset;
112				}
113				else {
114					self.offset.y = self.viewport.y.checked_sub(1).unwrap_or_default();
115				}
116			}
117		}
118	}
119
120	pub fn offset(&self, axis: Orientation) -> usize {
121		match axis {
122			Orientation::Horizontal => self.offset.x,
123			Orientation::Vertical => self.offset.y,
124		}
125	}
126
127	pub fn viewport(&self, axis: Orientation) -> usize {
128		match axis {
129			Orientation::Horizontal => self.viewport.x,
130			Orientation::Vertical => self.viewport.y,
131		}
132	}
133
134	pub fn set_scroll_viewer(&mut self, scroll_viewer: Weak<RefCell<ScrollViewer>>) {
135		self.scroll_viewer = scroll_viewer;
136	}
137}
138
139#[derive(Clone, Copy, Debug)]
140pub enum Direction {
141	Up,
142	Down,
143	Left,
144	Right,
145}
146
147#[derive(Copy, Clone, Debug)]
148pub enum ScrollKind {
149	Line,
150	Page,
151}
152
153/// Scrolling container which manages the internal content's scrolling
154///
155/// Design (optional Border):
156///  ```text
157/// ┌─────────┐
158/// │Child1  ▲│
159/// │Child2  ┃│
160/// │Child3   │
161/// │Child4   │
162/// │Child5  ▼│
163/// │◀━     ▶ │
164/// └─────────┘
165/// ```
166#[derive(Default)]
167pub struct ScrollViewer {
168	base: WidgetBase,
169	draw_bars: Vector2D<bool>,
170	content: Option<Rc<RefCell<dyn Scrollable>>>,
171	show_scrollbar: bool,
172	size: Vector2D<TSize>,
173}
174
175impl ScrollViewer {
176	pub const ARROW_UP: Grapheme = Grapheme::new_unchecked("▲", GlyphWidth::Half);
177	pub const ARROW_UP_DISABLED: Grapheme = Grapheme::new_unchecked("△", GlyphWidth::Half);
178	pub const BAR_MARKER_V: Grapheme = Grapheme::new_unchecked("┃", GlyphWidth::Half);
179	pub const ARROW_DOWN: Grapheme = Grapheme::new_unchecked("▼", GlyphWidth::Half);
180	pub const ARROW_DOWN_DISABLED: Grapheme = Grapheme::new_unchecked("▽", GlyphWidth::Half);
181
182	pub const ARROW_LEFT: Grapheme = Grapheme::new_unchecked("◀", GlyphWidth::Half);
183	pub const ARROW_LEFT_DISABLED: Grapheme = Grapheme::new_unchecked("◁", GlyphWidth::Half);
184	pub const BAR_MARKER_H: Grapheme = Grapheme::new_unchecked("━", GlyphWidth::Half);
185	pub const ARROW_RIGHT: Grapheme = Grapheme::new_unchecked("▶", GlyphWidth::Half);
186	pub const ARROW_RIGHT_DISABLED: Grapheme = Grapheme::new_unchecked("▷", GlyphWidth::Half);
187
188	pub fn set_content(rc: &Rc<RefCell<Self>>, mut content: Option<&Rc<RefCell<dyn Scrollable>>>) {
189		if let Some(old) = rc.borrow_mut().content.as_mut() {
190			old.borrow_mut().set_scroll_viewer(Weak::new());
191		}
192
193		if let Some(cnt) = content.as_mut() {
194			cnt.borrow_mut().set_scroll_viewer(Rc::downgrade(rc));
195		}
196		let brw = &mut rc.borrow_mut();
197		brw.content = content.cloned();
198		brw.provoke_changed_property(WindowProperty::Children);
199	}
200
201	pub fn set_scrollbar_visibility(&mut self, show: bool) {
202		self.show_scrollbar = show;
203		self.reevaluate_showbars();
204	}
205
206	fn reevaluate_showbars(&mut self) {
207		if let Some(cnt) = self.content.as_ref() {
208			let brw = cnt.borrow();
209			let old = self.draw_bars;
210			self.draw_bars = Vector2D::new(
211				self.size.x > 2 && brw.is_scrollable(Orientation::Vertical) && self.show_scrollbar,
212				self.size.y > 2
213					&& brw.is_scrollable(Orientation::Horizontal)
214					&& self.show_scrollbar,
215			);
216			if old != self.draw_bars {
217				self.provoke_changed_property(WindowProperty::Children);
218			}
219		}
220	}
221
222	fn render_scroller(
223		&self,
224		array: &mut dyn GlyphArray,
225		size: TPoint,
226		content: &dyn Scrollable,
227		orientation: Orientation,
228		other_bar: bool,
229	) {
230		let (arrow_prev, arrow_next, bar_marker);
231		let (arrow_prev_dis, arrow_next_dis);
232		let max_size;
233
234		match orientation {
235			Orientation::Horizontal => {
236				arrow_prev = Self::ARROW_LEFT;
237				arrow_prev_dis = Self::ARROW_LEFT_DISABLED;
238				arrow_next = Self::ARROW_RIGHT;
239				arrow_next_dis = Self::ARROW_RIGHT_DISABLED;
240				bar_marker = Self::BAR_MARKER_H;
241				max_size = size.x;
242			}
243			Orientation::Vertical => {
244				arrow_prev = Self::ARROW_UP;
245				arrow_prev_dis = Self::ARROW_UP_DISABLED;
246				arrow_next = Self::ARROW_DOWN;
247				arrow_next_dis = Self::ARROW_DOWN_DISABLED;
248				bar_marker = Self::BAR_MARKER_V;
249				max_size = size.y;
250			}
251		}
252
253		if max_size > 2 && content.is_scrollable(orientation) {
254			let offset = content.offset(orientation);
255			let viewport = content.viewport(orientation);
256
257			let mut arrow_up = array.get(TSize::MIN).unwrap();
258			if !self.base.enabled || offset == usize::MIN {
259				arrow_up.set_grapheme(arrow_prev_dis).ok();
260				if let Some(color) = self.base.colors.disabled {
261					arrow_up.set_fg(color);
262				}
263			}
264			else {
265				arrow_up.set_grapheme(arrow_prev).ok();
266			}
267			std::mem::drop(arrow_up);
268
269			let render_size = max_size - other_bar as TSize;
270			if max_size >= 2 {
271				let length = size[orientation as usize] - 2 - other_bar as TSize;
272				let start = (offset * length as usize + 1).div_ceil(viewport) as TSize;
273				let end = if offset + render_size as usize > viewport {
274					length + 1
275				}
276				else {
277					((offset + render_size as usize + 1) * length as usize).div_ceil(viewport)
278						as TSize
279				};
280
281				for idx in start..end + (start == end) as TSize {
282					let mut g = array.get(idx).unwrap();
283					g.set_grapheme(bar_marker).ok();
284					if let Some(color) = self.base.colors.disabled
285						&& !self.base.enabled
286					{
287						g.set_fg(color);
288					}
289				}
290			}
291
292			let mut arrow_down = array.get(max_size - 1 - other_bar as TSize).unwrap();
293			if !self.base.enabled || offset + render_size as usize >= viewport {
294				arrow_down.set_grapheme(arrow_next_dis).ok();
295				if let Some(color) = self.base.colors.disabled {
296					arrow_down.set_fg(color);
297				}
298			}
299			else {
300				arrow_down.set_grapheme(arrow_next).ok();
301			}
302		}
303	}
304
305
306	/// Can current [Scrollable] be scrolled horizontally / vertically
307	pub fn is_scrollable(&self, axis: Orientation) -> bool {
308		match self.content.as_ref() {
309			Some(sc) => sc.borrow().is_scrollable(axis),
310			None => false,
311		}
312	}
313
314	/// Scrolls page/line-wise (see [ScrollKind]) in the given direction
315	pub fn scroll(&mut self, direction: Direction, kind: ScrollKind) {
316		if let Some(sc) = self.content.as_ref() {
317			sc.borrow_mut().scroll(direction, kind);
318		}
319	}
320
321	/// Sets the horizontal / vertical offset
322	pub fn set_offset(&mut self, direction: Orientation, offset: usize) {
323		if let Some(sc) = self.content.as_ref() {
324			sc.borrow_mut().set_offset(direction, offset);
325		}
326	}
327
328	/// Horizontal / Vertical offset of the scrolled content
329	pub fn offset(&self, axis: Orientation) -> usize {
330		match self.content.as_ref() {
331			Some(sc) => sc.borrow().offset(axis),
332			None => usize::MIN,
333		}
334	}
335
336	/// Horizontal / Vertical size of the viewport for current [Scrollable]
337	pub fn viewport(&self, axis: Orientation) -> usize {
338		match self.content.as_ref() {
339			Some(sc) => sc.borrow().viewport(axis),
340			None => usize::MIN,
341		}
342	}
343}
344
345impl Window for ScrollViewer {
346	fn render(&self, canvas: &mut crate::canvas::Canvas) {
347		if let Some(content) = self.content.as_ref() {
348			let brw = content.borrow();
349			let size = canvas.size();
350			if self.draw_bars.x {
351				self.render_scroller(
352					&mut canvas.get_column(size.x - 1, GlyphWidth::Half).unwrap(),
353					size,
354					&*brw,
355					Orientation::Vertical,
356					self.draw_bars.y,
357				);
358			}
359			if self.draw_bars.y {
360				self.render_scroller(
361					&mut canvas.get_row(size.y - 1, GlyphWidth::Half).unwrap(),
362					size,
363					&*brw,
364					Orientation::Horizontal,
365					self.draw_bars.x,
366				);
367			}
368		}
369	}
370
371	fn handle_event(&mut self, event: &mut WindowEvent) {
372		if let Event::Resize(w, h) = event.raw() {
373			self.size = Vector2D::new(*w, *h);
374			self.reevaluate_showbars();
375		}
376	}
377
378	fn children(&mut self, mut builder: SubWindowBuilder) -> super::SubWindows {
379		if let Some(ch) = self.content.clone() {
380			let rect = builder.base_rect();
381			builder
382				.add_pair(
383					ch,
384					Some(
385						rect.subrect(
386							0,
387							0,
388							rect.size.x - self.draw_bars.x as TSize,
389							rect.size.y - self.draw_bars.y as TSize,
390						)
391						.unwrap(),
392					),
393				)
394				.unwrap();
395		}
396		builder.build().unwrap()
397	}
398
399	fn focus(&self) -> Option<WindowRef> {
400		self.content.clone().map(|x| x as Rc<RefCell<dyn Window>>)
401	}
402
403	fn is_enabled(&self) -> bool {
404		self.base.enabled
405	}
406}
407
408impl HasWindowUID for ScrollViewer {
409	fn uid(&self) -> super::WindowUID {
410		self.base.uid
411	}
412}
413
414impl WindowLayout for ScrollViewer {
415	fn desired_size(&self, available_size: TPoint) -> TPoint {
416		self.base.desired_size(available_size)
417	}
418
419	fn alignment(&self) -> (HorizontalAlignment, VerticalAlignment) {
420		self.base.alignment
421	}
422
423	fn border(&self) -> BorderStyle {
424		self.base.border
425	}
426
427	fn is_visible(&self) -> bool {
428		self.base.visibility
429	}
430
431	fn margin(&self) -> Thickness {
432		self.base.margin
433	}
434}
435
436impl Widget for ScrollViewer {
437	fn set_visibility(&mut self, visibility: bool) {
438		self.base.visibility = visibility;
439		self.provoke_changed_property(WindowProperty::IsVisible);
440	}
441
442	fn set_width(&mut self, width: Size) {
443		self.base.size.x = width;
444		self.provoke_changed_property(WindowProperty::Size);
445	}
446
447	fn set_height(&mut self, height: Size) {
448		self.base.size.y = height;
449		self.provoke_changed_property(WindowProperty::Size);
450	}
451
452	fn set_margin(&mut self, margin: Thickness) {
453		self.base.margin = margin;
454		self.provoke_changed_property(WindowProperty::Margin);
455	}
456
457	fn set_border(&mut self, border: BorderStyle) {
458		self.base.border = border;
459		self.provoke_changed_property(WindowProperty::Border);
460	}
461
462	fn set_alignment(&mut self, horizontal: HorizontalAlignment, vertical: VerticalAlignment) {
463		self.base.alignment = (horizontal, vertical);
464		self.provoke_changed_property(WindowProperty::Alignment);
465	}
466
467	fn set_enabled_state(&mut self, is_enabled: bool) {
468		self.base.enabled = is_enabled;
469		self.provoke_changed_property(WindowProperty::Children);
470	}
471
472	fn set_width_constraint(&mut self, width: SizeConstraint) {
473		self.base.constraints.x = width;
474		self.provoke_changed_property(WindowProperty::Size);
475	}
476
477	fn set_height_constraint(&mut self, height: SizeConstraint) {
478		self.base.constraints.y = height;
479		self.provoke_changed_property(WindowProperty::Size);
480	}
481}
482
483impl WidgetColors for ScrollViewer {
484	fn set_disabled_color(&mut self, color: Option<Color>) {
485		self.base.colors.disabled = color;
486	}
487
488	fn set_base_fg_color(&mut self, color: Option<Color>) {
489		self.base.colors.base_fg = color;
490	}
491
492	fn set_base_bg_color(&mut self, color: Option<Color>) {
493		self.base.colors.base_bg = color;
494	}
495}