Skip to main content

flux_tui/components/
button.rs

1use super::*;
2use crate::canvas::*;
3
4use crossterm::event::{Event, KeyCode};
5use std::num::NonZero;
6use std::rc::Rc;
7use unicode_width::UnicodeWidthStr;
8
9pub type ExecuteCallback = dyn Fn(&mut Button);
10pub type ButtonKeyHandler = fn(&mut Button, event: &mut WindowEvent);
11
12/// Button shows a text and executes a callback when pressing Enter (while focused)
13/// Focus highlights the inner text
14///
15/// Design (optional Border):
16/// ```text
17/// ┌──────┐
18/// │Button│
19/// └──────┘
20/// ```
21pub struct Button {
22	text: String,
23	text_alignment: HorizontalAlignment,
24	callback: Rc<ExecuteCallback>,
25	key_handler: ButtonKeyHandler,
26	has_focus: bool,
27	base: WidgetBase,
28}
29
30impl Default for Button {
31	fn default() -> Self {
32		let mut s = Self {
33			text: String::default(),
34			text_alignment: HorizontalAlignment::default(),
35			callback: Rc::new(Self::default_callback),
36			key_handler: Self::default_key_handler,
37			has_focus: false,
38			base: WidgetBase::default(),
39		};
40		s.base.constraints.y.min = NonZero::<TSize>::MIN;
41		s.base.constraints.y.max = Size::Fixed(NonZero::<TSize>::MIN);
42		s
43	}
44}
45
46impl Button {
47	pub fn default_callback(_: &mut Self) {}
48
49	pub fn default_key_handler(&mut self, event: &mut WindowEvent) {
50		if let Event::Key(k) = event.raw() {
51			if self.base.enabled && k.code == KeyCode::Enter {
52				self.execute();
53				event.handled = true;
54			}
55		}
56	}
57
58	/// Sets the callback, which is called when pressing `Enter`
59	pub fn set_callback<F: Fn(&mut Self) + 'static>(&mut self, callback: F) {
60		self.callback = Rc::new(callback);
61	}
62
63	pub fn set_key_handler(&mut self, f: ButtonKeyHandler) {
64		self.key_handler = f;
65	}
66
67	/// Sets the alignment for the inner text
68	pub fn set_text_alignment(&mut self, alignment: HorizontalAlignment) {
69		self.text_alignment = alignment;
70	}
71
72	/// Returns the internal text
73	pub fn get_text(&self) -> &str {
74		&self.text
75	}
76
77	/// Sets the internal shown text
78	pub fn set_text(&mut self, text: &str) {
79		self.text.clear();
80		self.text.push_str(text);
81	}
82
83	/// Executes the callback
84	pub fn execute(&mut self) {
85		let cb = self.callback.clone();
86		cb(self);
87	}
88}
89
90impl Window for Button {
91	fn render(&self, canvas: &mut Canvas) {
92		let mut row = canvas.get_row_variable_width(TSize::MIN).unwrap();
93		let width = self.text.width() as TSize;
94		let mut skip = TSize::MIN;
95
96		match self.text_alignment {
97			HorizontalAlignment::Center => {
98				if row.width() > width {
99					skip = (row.width() - width) / 2;
100				}
101			}
102			// default behaviour
103			HorizontalAlignment::Left => {}
104			HorizontalAlignment::Right => {
105				if row.width() > width {
106					skip = row.width() - width;
107				}
108			}
109		}
110
111		row.skip(skip);
112		row.add_string(
113			&self.text,
114			self.base.colors.base_fg,
115			self.base.colors.base_bg,
116			Style::Reverse.when(self.has_focus),
117			Some(VariableWidthGlyphRow::DOT3_REPLACEMENT),
118		)
119		.ok();
120		if let Some(color) = self.base.colors.disabled
121			&& !self.base.enabled
122		{
123			row.fill_foreground(color);
124		}
125	}
126
127	fn handle_event(&mut self, event: &mut WindowEvent) {
128		match event.raw() {
129			Event::FocusGained => self.has_focus = true,
130			Event::FocusLost => self.has_focus = false,
131			_ => (self.key_handler)(self, event),
132		}
133	}
134
135	fn is_enabled(&self) -> bool {
136		self.base.enabled
137	}
138}
139
140impl WindowLayout for Button {
141	fn desired_size(&self, available_size: TPoint) -> TPoint {
142		self.base.desired_size(available_size)
143	}
144
145	fn border(&self) -> BorderStyle {
146		self.base.border
147	}
148
149	fn alignment(&self) -> (HorizontalAlignment, VerticalAlignment) {
150		self.base.alignment
151	}
152
153	fn is_visible(&self) -> bool {
154		self.base.visibility
155	}
156
157	fn margin(&self) -> Thickness {
158		self.base.margin
159	}
160}
161
162impl HasWindowUID for Button {
163	fn uid(&self) -> WindowUID {
164		self.base.uid
165	}
166}
167
168impl Widget for Button {
169	fn set_visibility(&mut self, visibility: bool) {
170		self.base.visibility = visibility;
171		self.provoke_changed_property(WindowProperty::IsVisible);
172	}
173
174	fn set_width(&mut self, width: Size) {
175		self.base.size.x = width;
176		self.provoke_changed_property(WindowProperty::Size);
177	}
178
179	fn set_height(&mut self, _: Size) {}
180
181	fn set_margin(&mut self, margin: Thickness) {
182		self.base.margin = margin;
183		self.provoke_changed_property(WindowProperty::Margin);
184	}
185
186	fn set_border(&mut self, border: BorderStyle) {
187		self.base.border = border;
188		self.provoke_changed_property(WindowProperty::Border);
189	}
190
191	fn set_alignment(&mut self, horizontal: HorizontalAlignment, vertical: VerticalAlignment) {
192		self.base.alignment = (horizontal, vertical);
193		self.provoke_changed_property(WindowProperty::Alignment);
194	}
195
196	fn set_enabled_state(&mut self, is_enabled: bool) {
197		self.base.enabled = is_enabled;
198	}
199
200	fn set_width_constraint(&mut self, width: SizeConstraint) {
201		self.base.constraints.x = width;
202		self.provoke_changed_property(WindowProperty::Size);
203	}
204
205	fn set_height_constraint(&mut self, _: SizeConstraint) {}
206}
207
208impl WidgetColors for Button {
209	fn set_disabled_color(&mut self, color: Option<Color>) {
210		self.base.colors.disabled = color;
211	}
212
213	fn set_base_fg_color(&mut self, color: Option<Color>) {
214		self.base.colors.base_fg = color;
215	}
216
217	fn set_base_bg_color(&mut self, color: Option<Color>) {
218		self.base.colors.base_bg = color;
219	}
220}