Skip to main content

flux_tui/components/
dockpanel.rs

1use arrayvec::ArrayVec;
2use as_any::Downcast;
3use vector2d::Vector2D;
4
5use crate::{canvas::*, common::*, window::*};
6use std::rc::Rc;
7
8use super::*;
9
10pub type FocusChangedCallback = dyn Fn(&mut DockPanel);
11pub type DockPanelKeyHandler = fn(&mut DockPanel, event: &mut WindowEvent);
12
13#[derive(Debug, Clone, Copy, PartialEq)]
14#[repr(usize)]
15pub enum DockAlignment {
16	Top,
17	Bottom,
18	Left,
19	Right,
20	Center,
21}
22
23impl DockAlignment {
24	pub const VARIANTS: &'static [Self] = &[
25		DockAlignment::Top,
26		DockAlignment::Bottom,
27		DockAlignment::Left,
28		DockAlignment::Right,
29		DockAlignment::Center,
30	];
31}
32
33/// The center element takes most of the space, but at least 50% in width and height, depending how big the other elements are.
34/// The outer elements are technically computed within the layout first and the center is the last one. Each outer element
35/// should occupy at most 25% of the width/height - their relative size is relative to the maximum 25%. In case their size
36/// is fixed, the heuristic will choose MIN(\<fixed window size\>, \<25% of the dockpanels size\>).
37/// This control by default has no keybindings.
38/// ```text
39/// ┌───────────────────────┐
40/// │         Top           │
41/// ├──────┬────────┬───────┤
42/// │ Left │ Center │ Right │
43/// ├──────┴────────┴───────┤
44/// │        Bottom         │
45/// └───────────────────────┘
46/// ```
47pub struct DockPanel {
48	base: WidgetBase,
49	inner_border: bool,
50	children: [Option<WindowRef>; 5],
51	inner_borders: ArrayVec<Line2D<TSize>, 4>,
52	focus: DockAlignment,
53	key_handler: DockPanelKeyHandler,
54	callback: Rc<FocusChangedCallback>,
55}
56
57impl Default for DockPanel {
58	fn default() -> Self {
59		Self::new(false)
60	}
61}
62
63impl DockPanel {
64	/// The maximum size of an outer (non-center) child element
65	pub const MAX_CHILD_SIZE: Size = Size::Relative(Percent::from_int(25));
66
67	pub fn new(inner_border: bool) -> Self {
68		Self {
69			base: WidgetBase::default(),
70			inner_border,
71			children: [const { None }; 5],
72			inner_borders: ArrayVec::new(),
73			focus: DockAlignment::Center,
74			key_handler: Self::default_key_handler,
75			callback: Rc::new(Self::default_callback),
76		}
77	}
78
79	pub fn default_callback(_: &mut Self) {}
80
81	pub fn default_key_handler(&mut self, _: &mut WindowEvent) {}
82
83	pub fn set_selection_changed_callback<F: Fn(&mut Self) + 'static>(&mut self, callback: F) {
84		self.callback = Rc::new(callback);
85	}
86
87	pub fn set_key_handler(&mut self, f: DockPanelKeyHandler) {
88		self.key_handler = f;
89	}
90
91	pub fn set_inner_border(&mut self, inner_border: bool) {
92		self.inner_border = inner_border;
93		self.provoke_changed_property(WindowProperty::Children);
94	}
95
96	pub fn set_child(&mut self, dock: DockAlignment, child: Option<WindowRef>) {
97		if self.focus == dock && child.is_none() {
98			self.focus = DockAlignment::Center;
99		}
100		self.children[dock as usize] = child;
101		self.provoke_changed_property(WindowProperty::Children);
102	}
103
104	pub fn set_focus(&mut self, dock: DockAlignment) {
105		if self.focus != dock && self.children[dock as usize].is_some() {
106			self.focus = dock;
107			self.provoke_changed_property(WindowProperty::Focus);
108			let cb = self.callback.clone();
109			cb(self);
110		}
111	}
112
113	fn draw_border(window: &dyn Window, mut canvas: BorderCanvas) {
114		let slf: &Self = window.downcast_ref().unwrap();
115		if let BorderStyle::Preset { kind, bg, fg } = slf.base.border {
116			canvas.draw_preset_border(kind, fg, bg, "").unwrap();
117			let cts = kind.connector_style();
118			let size = canvas.size();
119			for l in slf.inner_borders.iter().filter(|x| x.is_horizontal()) {
120				canvas
121					.get_glyph(0, l.a.y + 1)
122					.unwrap()
123					.set_grapheme(cts[0])
124					.unwrap();
125				canvas
126					.get_glyph(size.x - 1, l.a.y + 1)
127					.unwrap()
128					.set_grapheme(cts[1])
129					.unwrap();
130			}
131		}
132	}
133}
134
135impl Window for DockPanel {
136	fn render(&self, canvas: &mut crate::canvas::Canvas) {
137		//Draw inner borders
138		if self.inner_border {
139			let kind = self.base.border.get_borderkind().unwrap_or_default();
140			let cts = kind.connector_style();
141			let lns = kind.line_style();
142			for line in self.inner_borders.iter() {
143				if line.is_vertical() {
144					if let Some(mut column) = canvas.get_column(line.a.x, GlyphWidth::Half) {
145						for pos in line.iter_points() {
146							column.get(pos.y).unwrap().set_grapheme(lns[1]).unwrap();
147						}
148					}
149				}
150				else if let Some(mut row) = canvas.get_row(line.a.y, GlyphWidth::Half) {
151					for pos in line.iter_points() {
152						row.get(pos.x).unwrap().set_grapheme(lns[0]).unwrap();
153					}
154				}
155			}
156
157			for vert in self.inner_borders.iter().filter(|x| x.is_vertical()) {
158				for hori in self.inner_borders.iter().filter(|x| x.is_horizontal()) {
159					if hori.a.y + 1 == vert.a.y {
160						if let Some(mut gl) =
161							canvas.get_glyph(vert.a.x, vert.a.y - 1, GlyphWidth::Half)
162						{
163							gl.set_grapheme(cts[2]).unwrap();
164						}
165					}
166					else if hori.a.y == vert.b.y {
167						if let Some(mut gl) = canvas.get_glyph(vert.b.x, vert.b.y, GlyphWidth::Half)
168						{
169							gl.set_grapheme(cts[3]).unwrap();
170						}
171					}
172				}
173			}
174		}
175	}
176
177	fn handle_event(&mut self, event: &mut WindowEvent) {
178		(self.key_handler)(self, event)
179	}
180
181	fn children(&mut self, mut builder: SubWindowBuilder) -> SubWindows {
182		builder.disable_overlapping();
183		let mut base_rect = builder.base_rect();
184
185		let max_x = Self::MAX_CHILD_SIZE.get_size(base_rect.size.x);
186		let max_y = Self::MAX_CHILD_SIZE.get_size(base_rect.size.y);
187
188		let draw_border = self.inner_border && base_rect.size.x > 4 && base_rect.size.y > 4;
189
190		self.inner_borders.clear();
191		for dock in DockAlignment::VARIANTS.iter() {
192			if let Some(child) = self.children[*dock as usize].clone() {
193				let rect;
194				let size = child.borrow().desired_size(Vector2D::new(max_x, max_y));
195				//Retrieves the best possible size for the outer (non-center) children
196				let border = draw_border as TSize;
197				if size.x * size.y == 0 {
198					continue;
199				}
200				match dock {
201					DockAlignment::Top => {
202						rect = base_rect.subrect(TSize::MIN, TSize::MIN, base_rect.size.x, size.y);
203						if draw_border {
204							self.inner_borders.push(Line2D::new(
205								TSize::MIN,
206								size.y,
207								base_rect.end().x,
208								size.y,
209							));
210						}
211						base_rect = base_rect
212							.subrect(
213								TSize::MIN,
214								size.y + border,
215								base_rect.size.x,
216								base_rect.size.y - (size.y + border),
217							)
218							.unwrap();
219					}
220					DockAlignment::Bottom => {
221						rect = base_rect.subrect(
222							TSize::MIN,
223							base_rect.size.y - size.y,
224							base_rect.size.x,
225							size.y,
226						);
227						if draw_border {
228							self.inner_borders.push(Line2D::new(
229								TSize::MIN,
230								base_rect.end().y - (size.y + border),
231								base_rect.end().x,
232								base_rect.end().y - (size.y + border),
233							));
234						}
235
236						base_rect = base_rect
237							.subrect(
238								TSize::MIN,
239								TSize::MIN,
240								base_rect.size.x,
241								base_rect.size.y - (size.y + border),
242							)
243							.unwrap();
244					}
245					DockAlignment::Left => {
246						rect = base_rect.subrect(TSize::MIN, TSize::MIN, size.x, base_rect.size.y);
247						if draw_border {
248							self.inner_borders.push(Line2D::new(
249								size.x,
250								base_rect.start.y,
251								size.x,
252								base_rect.end().y,
253							));
254						}
255						base_rect = base_rect
256							.subrect(
257								size.x + border,
258								TSize::MIN,
259								base_rect.size.x - (size.x + border),
260								base_rect.size.y,
261							)
262							.unwrap();
263					}
264					DockAlignment::Right => {
265						rect = base_rect.subrect(
266							base_rect.size.x - size.x,
267							TSize::MIN,
268							size.x,
269							base_rect.size.y,
270						);
271						if draw_border {
272							self.inner_borders.push(Line2D::new(
273								base_rect.end().x - (size.x + border),
274								base_rect.start.y,
275								base_rect.end().x - (size.x + border),
276								base_rect.end().y,
277							));
278						}
279						base_rect = base_rect
280							.subrect(
281								TSize::MIN,
282								TSize::MIN,
283								base_rect.size.x - (size.x + border),
284								base_rect.size.y,
285							)
286							.unwrap();
287					}
288					DockAlignment::Center => {
289						rect = Ok(base_rect);
290					}
291				}
292				builder.add_rect(rect.unwrap()).unwrap();
293				builder.add_child(child);
294			}
295		}
296
297		builder.build().unwrap()
298	}
299
300	fn focus(&self) -> Option<WindowRef> {
301		self.children[self.focus as usize].clone()
302	}
303}
304
305impl HasWindowUID for DockPanel {
306	fn uid(&self) -> WindowUID {
307		self.base.uid
308	}
309}
310
311impl WindowLayout for DockPanel {
312	fn desired_size(&self, available_size: TPoint) -> TPoint {
313		self.base.desired_size(available_size)
314	}
315
316	fn alignment(&self) -> (HorizontalAlignment, VerticalAlignment) {
317		self.base.alignment
318	}
319
320	fn margin(&self) -> Thickness {
321		self.base.margin
322	}
323
324	fn border(&self) -> BorderStyle {
325		BorderStyle::custom(Self::draw_border, Thickness::from(1))
326	}
327
328	fn is_visible(&self) -> bool {
329		self.base.visibility
330	}
331}
332
333impl Widget for DockPanel {
334	fn set_alignment(&mut self, horizontal: HorizontalAlignment, vertical: VerticalAlignment) {
335		self.base.alignment = (horizontal, vertical);
336		self.provoke_changed_property(WindowProperty::Alignment);
337	}
338
339	fn set_visibility(&mut self, visibility: bool) {
340		self.base.visibility = visibility;
341		self.provoke_changed_property(WindowProperty::IsVisible);
342	}
343
344	fn set_width(&mut self, width: Size) {
345		self.base.size.x = width;
346		self.provoke_changed_property(WindowProperty::Size);
347	}
348
349	fn set_height(&mut self, height: Size) {
350		self.base.size.y = height;
351		self.provoke_changed_property(WindowProperty::Size);
352	}
353
354	fn set_margin(&mut self, margin: Thickness) {
355		self.base.margin = margin;
356		self.provoke_changed_property(WindowProperty::Margin);
357	}
358
359	fn set_border(&mut self, border: BorderStyle) {
360		self.base.border = border;
361		self.provoke_changed_property(WindowProperty::Border);
362	}
363
364	fn set_enabled_state(&mut self, is_enabled: bool) {
365		self.base.enabled = is_enabled;
366	}
367
368	fn set_width_constraint(&mut self, width: SizeConstraint) {
369		self.base.constraints.x = width;
370		self.provoke_changed_property(WindowProperty::Size);
371	}
372
373	fn set_height_constraint(&mut self, height: SizeConstraint) {
374		self.base.constraints.y = height;
375		self.provoke_changed_property(WindowProperty::Size);
376	}
377}
378
379impl WidgetColors for DockPanel {
380	fn set_disabled_color(&mut self, color: Option<Color>) {
381		self.base.colors.disabled = color;
382	}
383
384	fn set_base_fg_color(&mut self, color: Option<Color>) {
385		self.base.colors.base_fg = color;
386	}
387
388	fn set_base_bg_color(&mut self, color: Option<Color>) {
389		self.base.colors.base_bg = color;
390	}
391}