Skip to main content

flux_tui/components/
stackpanel.rs

1use super::*;
2use crate::canvas::*;
3
4use std::{
5	cell::RefCell,
6	rc::{Rc, Weak},
7};
8
9use crossterm::event::{KeyCode, KeyModifiers};
10
11pub type StackpanelSelectionChangedCallback = dyn Fn(&mut Stackpanel);
12pub type StackpanelKeyHandler = fn(&mut Stackpanel, event: &mut WindowEvent);
13
14#[derive(Clone)]
15struct Child {
16	rc: WindowRef,
17	// Offset + len on either x or y axis, depending on Stackpanel.orientation
18	offset: usize,
19	len: usize,
20}
21
22impl Child {
23	fn new(rc: WindowRef) -> Self {
24		Self {
25			rc,
26			offset: usize::MIN,
27			len: usize::MIN,
28		}
29	}
30}
31
32struct Children {
33	items: Vec<Child>,
34	focus: usize,
35	alignment: Alignment,
36}
37
38impl Default for Children {
39	fn default() -> Self {
40		Self {
41			items: Vec::default(),
42			focus: usize::MIN,
43			alignment: Alignment::Center,
44		}
45	}
46}
47
48//TODO: How to scroll partially?? Extension for Canvas/Rect/etc.?
49/// Stackpanel will group multiple windows and arrange them according to the Orientation.
50/// It will also allow movement between them using the arrow keys.
51/// Default margin is 1.
52///
53/// Design (optional Border, vertical):
54///  ```text
55/// ┌─────────┐
56/// │Content A│
57/// │         │
58/// │Content B│
59/// └─────────┘
60/// ```
61///
62/// Horizontal:
63/// ```text
64/// ┌─────────────────────┐
65/// │Content A   Content B│
66/// └─────────────────────┘
67/// ```
68pub struct Stackpanel {
69	orientation: Orientation,
70	margin: u8,
71	children: Children,
72	base: WidgetBase,
73	scroll: ScrollBase,
74	callback: Rc<StackpanelSelectionChangedCallback>,
75	key_handler: StackpanelKeyHandler,
76}
77
78impl Default for Stackpanel {
79	fn default() -> Self {
80		Self::new(Orientation::Vertical)
81	}
82}
83
84impl Stackpanel {
85	pub fn default_callback(_: &mut Self) {}
86
87	pub fn default_key_handler(&mut self, event: &mut WindowEvent) {
88		if let Event::Key(k) = event.raw() {
89			let scroll_mode =
90				k.modifiers.contains(KeyModifiers::CONTROL) && self.has_scroll_viewer();
91			match self.orientation {
92				Orientation::Horizontal => match k.code {
93					KeyCode::Left => {
94						self.scroll(Direction::Left, ScrollKind::Line, scroll_mode);
95						event.handled = true;
96					}
97					KeyCode::Right => {
98						self.scroll(Direction::Right, ScrollKind::Line, scroll_mode);
99						event.handled = true;
100					}
101					KeyCode::PageUp => {
102						self.scroll(Direction::Left, ScrollKind::Page, scroll_mode);
103						event.handled = true;
104					}
105					KeyCode::PageDown => {
106						self.scroll(Direction::Right, ScrollKind::Page, scroll_mode);
107						event.handled = true;
108					}
109					_ => {}
110				},
111				Orientation::Vertical => match k.code {
112					KeyCode::Up => {
113						self.scroll(Direction::Up, ScrollKind::Line, scroll_mode);
114						event.handled = true;
115					}
116					KeyCode::Down => {
117						self.scroll(Direction::Down, ScrollKind::Line, scroll_mode);
118						event.handled = true;
119					}
120					KeyCode::PageUp => {
121						self.scroll(Direction::Up, ScrollKind::Page, scroll_mode);
122						event.handled = true;
123					}
124					KeyCode::PageDown => {
125						self.scroll(Direction::Down, ScrollKind::Page, scroll_mode);
126						event.handled = true;
127					}
128					_ => {}
129				},
130			}
131		}
132	}
133
134	/// Constructs a new Stackpanel with the given [Orientation]
135	pub fn new(orientation: Orientation) -> Self {
136		Self {
137			orientation,
138			margin: 1,
139			children: Children::default(),
140			base: WidgetBase::default(),
141			scroll: ScrollBase::default(),
142			callback: Rc::new(Self::default_callback),
143			key_handler: Self::default_key_handler,
144		}
145	}
146
147	pub fn set_selection_changed_callback<F: Fn(&mut Self) + 'static>(&mut self, callback: F) {
148		self.callback = Rc::new(callback);
149	}
150
151	pub fn set_key_handler(&mut self, f: StackpanelKeyHandler) {
152		self.key_handler = f;
153	}
154
155	/// Sets the alignment for each child/item within the [Stackpanel]
156	pub fn set_item_alignment(&mut self, alignment: Alignment) {
157		self.children.alignment = alignment;
158		self.provoke_changed_property(WindowProperty::Children);
159	}
160
161	pub fn set_orientation(&mut self, orientation: Orientation) {
162		self.orientation = orientation;
163		self.provoke_changed_property(WindowProperty::Children);
164	}
165
166	/// Sets the internal childrens focus
167	pub fn set_focus(&mut self, item: &Rc<RefCell<dyn Window>>) {
168		let uid = item.borrow().uid();
169		self.children.focus = self
170			.children
171			.items
172			.iter()
173			.position(|x| x.rc.borrow().uid() == uid)
174			.unwrap_or_default();
175		self.provoke_changed_property(WindowProperty::Focus);
176	}
177
178	/// Sets the offset between each child
179	pub fn set_child_margin(&mut self, margin: u8) {
180		self.margin = margin;
181	}
182
183	fn is_selection_visible(&self) -> bool {
184		let idx = self.orientation as usize;
185		let focus = &self.children.items[self.children.focus];
186		focus.offset >= self.scroll.offset[idx]
187			&& (focus.offset + focus.len)
188				< (self.scroll.offset[idx] + self.scroll.render_size[idx] as usize)
189	}
190
191	fn make_selection_visible(&mut self) {
192		let child = &self.children.items[self.children.focus];
193		let render_size = self.scroll.render_size[self.orientation as usize];
194		if let Some(new_offset) = self
195			.children
196			.items
197			.iter()
198			.map(|x| x.offset)
199			.find(|x| x + render_size as usize >= child.offset + child.len)
200		{
201			self.scroll.set_offset(self.orientation, new_offset);
202			self.provoke_changed_property(WindowProperty::Children);
203		}
204	}
205
206	/// scroll_only specifies whether selection should also change if only the view changes
207	pub fn scroll(&mut self, direction: Direction, kind: ScrollKind, scroll_only: bool) {
208		match kind {
209			ScrollKind::Line => match direction {
210				Direction::Left | Direction::Up => self.move_previous(scroll_only),
211				Direction::Right | Direction::Down => self.move_next(scroll_only),
212			},
213			ScrollKind::Page => match scroll_only {
214				true => self.scroll.scroll(direction, kind),
215				false => {
216					let delta = self.scroll.render_size[self.orientation as usize] as usize;
217					let min_offset = match direction {
218						Direction::Left | Direction::Up => self.scroll.offset
219							[self.orientation as usize]
220							.checked_sub(delta)
221							.unwrap_or_default(),
222						Direction::Right | Direction::Down => {
223							self.scroll.offset[self.orientation as usize] + delta
224						}
225					};
226					if let Some(index) = self
227						.children
228						.items
229						.iter()
230						.position(|x| x.offset >= min_offset)
231					{
232						self.children.focus = index;
233						self.provoke_changed_property(WindowProperty::Focus);
234
235						self.selection_changed();
236
237						self.scroll.set_offset(
238							self.orientation,
239							self.children.items[self.children.focus].offset,
240						);
241						self.provoke_changed_property(WindowProperty::Children);
242					}
243				}
244			},
245		}
246	}
247
248	fn move_next(&mut self, scroll_only: bool) {
249		match scroll_only {
250			true => {
251				if let Some(new_offset) = self
252					.children
253					.items
254					.iter()
255					.map(|x| x.offset)
256					.find(|x| *x > self.scroll.offset[self.orientation as usize])
257				{
258					self.scroll.set_offset(self.orientation, new_offset);
259					self.provoke_changed_property(WindowProperty::Children);
260				}
261			}
262			false => {
263				if self.children.focus < self.children.items.len() - 1 {
264					if let Some(idx) = self.children.items[self.children.focus + 1..]
265						.iter()
266						.position(|x| x.rc.borrow_mut().is_enabled())
267					{
268						if !self.is_selection_visible() {
269							self.make_selection_visible();
270						}
271
272						self.children.focus = self.children.focus + 1 + idx;
273						self.provoke_changed_property(WindowProperty::Focus);
274
275						self.selection_changed();
276
277						if self.has_scroll_viewer() {
278							let idx = self.orientation as usize;
279							let max =
280								self.scroll.offset[idx] + self.scroll.render_size[idx] as usize;
281							let child = &self.children.items[self.children.focus];
282							if child.offset >= max || child.offset + child.len >= max {
283								let render_size =
284									self.scroll.render_size[self.orientation as usize];
285								if let Some(new_offset) =
286									self.children.items.iter().map(|x| x.offset).find(|x| {
287										x + render_size as usize >= child.offset + child.len
288									}) {
289									self.scroll.set_offset(self.orientation, new_offset);
290									self.provoke_changed_property(WindowProperty::Children);
291								}
292							}
293						}
294					}
295				}
296			}
297		}
298	}
299
300	fn move_previous(&mut self, scroll_only: bool) {
301		match scroll_only {
302			true => {
303				if let Some(new_offset) = self
304					.children
305					.items
306					.iter()
307					.rev()
308					.map(|x| x.offset)
309					.find(|x| *x < self.scroll.offset[self.orientation as usize])
310				{
311					self.scroll.set_offset(self.orientation, new_offset);
312					self.provoke_changed_property(WindowProperty::Children);
313				}
314			}
315			false => {
316				if self.children.focus > usize::MIN {
317					if let Some(idx) = self.children.items[..self.children.focus]
318						.iter()
319						.rev()
320						.position(|x| x.rc.borrow_mut().is_enabled())
321					{
322						if !self.is_selection_visible() {
323							self.make_selection_visible();
324						}
325
326						self.children.focus = (self.children.focus - 1) - idx;
327						self.provoke_changed_property(WindowProperty::Focus);
328
329						self.selection_changed();
330
331						if self.has_scroll_viewer() {
332							let idx = self.orientation as usize;
333							let min = self.scroll.offset[idx];
334							let offset = self.children.items[self.children.focus].offset;
335							if offset < min {
336								self.scroll.set_offset(self.orientation, offset);
337								self.provoke_changed_property(WindowProperty::Children);
338							}
339						}
340					}
341				}
342			}
343		}
344	}
345
346	pub fn has_scroll_viewer(&self) -> bool {
347		self.scroll.scroll_viewer.strong_count() > usize::MIN
348	}
349
350	fn retrieve_children(&mut self, builder: &mut SubWindowBuilder) {
351		let has_sv = self.has_scroll_viewer();
352		let base_rect = builder.base_rect();
353		self.scroll.viewport = Default::default();
354		self.scroll.render_size = base_rect.size;
355
356		let full_size = match self.orientation {
357			Orientation::Horizontal => base_rect
358				.size
359				.x
360				.checked_sub(self.children.items.len() as TSize - 1)
361				.unwrap_or(base_rect.size.x),
362			Orientation::Vertical => base_rect
363				.size
364				.y
365				.checked_sub(self.children.items.len() as TSize - 1)
366				.unwrap_or(base_rect.size.y),
367		};
368		let fixed_size = match full_size
369			.checked_div(self.children.items.len() as TSize)
370			.unwrap_or_default()
371		{
372			TSize::MIN => 1,
373			v => v,
374		};
375
376		let item_count = self.children.items.len();
377
378		match self.orientation {
379			Orientation::Horizontal => {
380				for (n, item) in self.children.items.iter_mut().enumerate() {
381					let brw = item.rc.borrow();
382					let tn = brw.border().thickness();
383					let size = brw.desired_size(Vector2D::new(base_rect.size.x, base_rect.size.y));
384					let len = match has_sv {
385						true => size.x + tn.width(),
386						false => fixed_size,
387					} as usize;
388					let start = match self.children.alignment {
389						Alignment::LowerBound => TSize::MIN,
390						Alignment::Center => (base_rect.size.y - size.y) / 2,
391						Alignment::HigherBound => base_rect.size.y - size.y,
392					};
393					if self.scroll.viewport.x >= self.scroll.offset.x {
394						match base_rect.subrect(
395							(self.scroll.viewport.x - self.scroll.offset.x) as TSize,
396							start,
397							len as TSize,
398							size.y,
399						) {
400							Ok(rect) => {
401								builder.add_rect(rect).unwrap();
402								builder.add_child(item.rc.clone());
403							}
404							Err(_) => {
405								if !has_sv {
406									self.scroll.viewport.x += len;
407									break;
408								}
409							}
410						}
411					}
412					item.offset = self.scroll.viewport.y;
413					item.len = len;
414					self.scroll.viewport.x += len;
415					if n < item_count - 1 {
416						self.scroll.viewport.x += self.margin as usize;
417					}
418				}
419				self.scroll.viewport.y = base_rect.size.y as usize;
420			}
421			Orientation::Vertical => {
422				for (n, item) in self.children.items.iter_mut().enumerate() {
423					let brw = item.rc.borrow();
424					let tn = brw.border().thickness();
425					let size = brw.desired_size(Vector2D::new(base_rect.size.x, base_rect.size.y));
426					let len = match has_sv {
427						true => size.y + tn.height(),
428						false => fixed_size,
429					} as usize;
430					let start = match self.children.alignment {
431						Alignment::LowerBound => TSize::MIN,
432						Alignment::Center => (base_rect.size.x - size.x) / 2,
433						Alignment::HigherBound => base_rect.size.x - size.x,
434					};
435					if self.scroll.viewport.y >= self.scroll.offset.y {
436						match base_rect.subrect(
437							start,
438							(self.scroll.viewport.y - self.scroll.offset.y) as TSize,
439							size.x,
440							len as TSize,
441						) {
442							Ok(rect) => {
443								builder.add_rect(rect).unwrap();
444								builder.add_child(item.rc.clone());
445							}
446							Err(_) => {
447								if !has_sv {
448									self.scroll.viewport.y += len;
449									break;
450								}
451							}
452						}
453					}
454					item.offset = self.scroll.viewport.y;
455					item.len = len;
456					self.scroll.viewport.y += len;
457					//Skip margin for last item
458					if n < item_count - 1 {
459						self.scroll.viewport.y += self.margin as usize;
460					}
461				}
462				self.scroll.viewport.x = base_rect.size.x as usize;
463			}
464		}
465	}
466
467	fn selection_changed(&mut self) {
468		let cb = self.callback.clone();
469		cb(self);
470	}
471}
472
473impl Scrollable for Stackpanel {
474	fn is_scrollable(&self, axis: Orientation) -> bool {
475		self.scroll.is_scrollable(axis)
476	}
477
478	fn scroll(&mut self, direction: Direction, kind: ScrollKind) {
479		self.scroll(direction, kind, false);
480	}
481
482	fn set_offset(&mut self, direction: Orientation, offset: usize) {
483		self.scroll.set_offset(direction, offset);
484	}
485
486	fn offset(&self, axis: Orientation) -> usize {
487		self.scroll.offset[axis as usize]
488	}
489
490	fn viewport(&self, axis: Orientation) -> usize {
491		self.scroll.viewport[axis as usize]
492	}
493
494	fn set_scroll_viewer(&mut self, scroll_viewer: Weak<RefCell<ScrollViewer>>) {
495		self.scroll.set_scroll_viewer(scroll_viewer);
496		self.provoke_changed_property(WindowProperty::Children);
497	}
498}
499
500impl ItemCollection<WindowRef> for Stackpanel {
501	type Index = usize;
502
503	/// Adds a child to the internal array
504	fn add_item(&mut self, item: WindowRef) {
505		self.children.items.push(Child::new(item));
506		self.provoke_changed_property(WindowProperty::Children);
507	}
508
509	/// Removes a single child from the internal array
510	fn remove_item(&mut self, item: &WindowRef) {
511		let uid = item.borrow().uid();
512		if let Some(index) = self
513			.children
514			.items
515			.iter()
516			.position(|x| x.rc.borrow().uid() == uid)
517		{
518			self.children.items.remove(index);
519			if self.children.focus == index {
520				self.children.focus = usize::MIN;
521				self.selection_changed();
522				self.provoke_changed_property(WindowProperty::Focus);
523			}
524			self.provoke_changed_property(WindowProperty::Children);
525		}
526	}
527
528	fn remove_at(&mut self, index: Self::Index) -> WindowRef {
529		self.children.items.remove(index).rc
530	}
531
532	/// Returns a child by its index
533	fn get_item(&mut self, index: Self::Index) -> Option<&WindowRef> {
534		match self.children.items.get(index) {
535			Some(item) => Some(&item.rc),
536			None => None,
537		}
538	}
539
540	/// Removes all children from the internal array
541	fn clear_items(&mut self) {
542		if !self.children.items.is_empty() {
543			self.children.items.clear();
544			self.provoke_changed_property(WindowProperty::Children);
545			self.provoke_changed_property(WindowProperty::Focus);
546			self.selection_changed();
547		}
548	}
549
550	/// Returns the number of children
551	fn items(&self) -> Self::Index {
552		self.children.items.len()
553	}
554}
555
556impl Window for Stackpanel {
557	fn render(&self, canvas: &mut Canvas) {
558		if let Some(color) = self.base.colors.disabled
559			&& !self.base.enabled
560		{
561			canvas.fill_foreground(color);
562		}
563	}
564
565	fn children(&mut self, mut builder: SubWindowBuilder) -> SubWindows {
566		if self.children.items.is_empty() {
567			return SubWindows::default();
568		}
569
570		builder.disable_overlapping();
571		self.retrieve_children(&mut builder);
572		builder.build().unwrap()
573	}
574
575	fn handle_event(&mut self, event: &mut WindowEvent) {
576		(self.key_handler)(self, event);
577	}
578
579	fn focus(&self) -> Option<WindowRef> {
580		match self.base.enabled {
581			true => self
582				.children
583				.items
584				.get(self.children.focus)
585				.cloned()
586				.map(|x| x.rc),
587			false => None,
588		}
589	}
590
591	fn is_enabled(&self) -> bool {
592		self.base.enabled
593	}
594}
595
596impl WindowLayout for Stackpanel {
597	fn desired_size(&self, available_size: TPoint) -> TPoint {
598		self.base.desired_size(available_size)
599	}
600
601	fn alignment(&self) -> (HorizontalAlignment, VerticalAlignment) {
602		self.base.alignment
603	}
604
605	fn border(&self) -> BorderStyle {
606		self.base.border
607	}
608
609	fn is_visible(&self) -> bool {
610		self.base.visibility
611	}
612
613	fn margin(&self) -> Thickness {
614		self.base.margin
615	}
616}
617
618impl HasWindowUID for Stackpanel {
619	fn uid(&self) -> WindowUID {
620		self.base.uid
621	}
622}
623
624impl Widget for Stackpanel {
625	fn set_visibility(&mut self, visibility: bool) {
626		self.provoke_changed_property(WindowProperty::IsVisible);
627		self.base.visibility = visibility;
628	}
629
630	fn set_width(&mut self, width: Size) {
631		self.base.size.x = width;
632		self.provoke_changed_property(WindowProperty::Size);
633	}
634
635	fn set_height(&mut self, height: Size) {
636		self.base.size.y = height;
637		self.provoke_changed_property(WindowProperty::Size);
638	}
639
640	fn set_margin(&mut self, margin: Thickness) {
641		self.base.margin = margin;
642		self.provoke_changed_property(WindowProperty::Margin);
643	}
644
645	fn set_border(&mut self, border: BorderStyle) {
646		self.base.border = border;
647		self.provoke_changed_property(WindowProperty::Border);
648	}
649
650	fn set_alignment(&mut self, horizontal: HorizontalAlignment, vertical: VerticalAlignment) {
651		self.base.alignment = (horizontal, vertical);
652		self.provoke_changed_property(WindowProperty::Alignment);
653	}
654
655	fn set_enabled_state(&mut self, is_enabled: bool) {
656		self.base.enabled = is_enabled;
657		self.provoke_changed_property(WindowProperty::Children);
658	}
659
660	fn set_width_constraint(&mut self, width: SizeConstraint) {
661		self.base.constraints.x = width;
662		self.provoke_changed_property(WindowProperty::Size);
663	}
664
665	fn set_height_constraint(&mut self, height: SizeConstraint) {
666		self.base.constraints.y = height;
667		self.provoke_changed_property(WindowProperty::Size);
668	}
669}
670
671impl WidgetColors for Stackpanel {
672	fn set_disabled_color(&mut self, color: Option<Color>) {
673		self.base.colors.disabled = color;
674	}
675
676	fn set_base_fg_color(&mut self, color: Option<Color>) {
677		self.base.colors.base_fg = color;
678	}
679
680	fn set_base_bg_color(&mut self, color: Option<Color>) {
681		self.base.colors.base_bg = color;
682	}
683}