Skip to main content

flux_tui/components/
numeric.rs

1use super::*;
2use crate::canvas::*;
3
4use crossterm::event::{Event, KeyCode, KeyModifiers};
5use std::rc::Rc;
6
7pub type NumericValueChangedCallback = dyn Fn(&mut Numeric);
8pub type NumericKeyHandler = fn(&mut Numeric, event: &mut WindowEvent);
9
10
11///
12/// Numerical Up/Down component, where a number can be changed by stepping up or down with a given
13/// stepcount
14/// Design:
15/// ```text
16/// -128▼▲
17/// ```
18///
19pub struct Numeric {
20	base: WidgetBase,
21	has_focus: bool,
22	value: i32,
23	stepcount: u8,
24	radix: Radix,
25	callback: Rc<NumericValueChangedCallback>,
26	key_handler: NumericKeyHandler,
27	focus_color: Option<Color>,
28	min_range: i32,
29	max_range: i32,
30}
31
32impl Default for Numeric {
33	fn default() -> Self {
34		let mut slf = Self {
35			base: WidgetBase::default(),
36			has_focus: false,
37			value: 0,
38			stepcount: 1,
39			radix: Radix::Decimal,
40			callback: Rc::new(Self::default_callback),
41			key_handler: Self::default_key_handler,
42			focus_color: None,
43			min_range: i32::MIN,
44			max_range: i32::MAX,
45		};
46		slf.base.constraints.x.min = NonZero::new(3).unwrap();
47		slf.value_changed_hook();
48		slf
49	}
50}
51
52impl Numeric {
53	pub const ARROW_UP: Grapheme = ScrollViewer::ARROW_UP;
54	pub const ARROW_DOWN: Grapheme = ScrollViewer::ARROW_DOWN;
55	pub const ARROW_UP_DISABLED: Grapheme = ScrollViewer::ARROW_UP_DISABLED;
56	pub const ARROW_DOWN_DISABLED: Grapheme = ScrollViewer::ARROW_DOWN_DISABLED;
57	pub const NUMERIC_PLACEHOLDER: Grapheme = Grapheme::new_unchecked("x", GlyphWidth::Half);
58
59	pub fn default_key_handler(&mut self, event: &mut WindowEvent) {
60		if let Event::Key(k) = event.raw() {
61			if k.modifiers.contains(KeyModifiers::ALT) {
62				if k.code == KeyCode::Up {
63					self.step_up();
64					event.handled = true;
65				}
66				else if k.code == KeyCode::Down {
67					self.step_down();
68					event.handled = true;
69				}
70			}
71		}
72	}
73
74	pub fn default_callback(_: &mut Self) {}
75
76	pub fn set_changed_value_callback<F: Fn(&mut Numeric) + 'static>(&mut self, callback: F) {
77		self.callback = Rc::new(callback);
78	}
79
80	pub fn set_key_handler(&mut self, f: NumericKeyHandler) {
81		self.key_handler = f;
82	}
83
84	pub fn set_min_range(&mut self, min_range: i32) {
85		if self.min_range < self.max_range {
86			self.min_range = min_range;
87			if !(self.min_range..self.max_range).contains(&self.value) {
88				self.value = self.min_range;
89				self.value_changed_hook();
90			}
91		}
92	}
93
94	pub fn set_max_range(&mut self, max_range: i32) {
95		if self.max_range > self.min_range {
96			self.max_range = max_range;
97			if !(self.min_range..self.max_range).contains(&self.value) {
98				self.value = self.min_range;
99				self.value_changed_hook();
100			}
101		}
102	}
103
104	pub fn set_value(&mut self, value: i32) {
105		self.value = value;
106		self.value_changed_hook();
107	}
108
109	pub fn get_value(&self) -> i32 {
110		self.value
111	}
112
113	pub fn set_base(&mut self, base: Radix) {
114		if base != self.radix {
115			self.radix = base;
116			self.value_changed_hook();
117		}
118	}
119
120	pub fn get_base(&self) -> Radix {
121		self.radix
122	}
123
124	pub fn step_up(&mut self) {
125		let cb = self.callback.clone();
126		if self.value <= self.max_range - self.stepcount as i32 {
127			self.value += self.stepcount as i32;
128		}
129		else {
130			self.value = self.max_range;
131		}
132		self.value_changed_hook();
133		cb(self);
134	}
135
136	pub fn step_down(&mut self) {
137		let cb = self.callback.clone();
138		if self.value > self.min_range + self.stepcount as i32 {
139			self.value -= self.stepcount as i32;
140		}
141		else {
142			self.value = self.min_range;
143		}
144		self.value_changed_hook();
145		cb(self);
146	}
147
148	pub fn get_stepcount(&self) -> u8 {
149		self.stepcount
150	}
151
152	pub fn set_stepcount(&mut self, stepcount: u8) {
153		self.stepcount = stepcount;
154	}
155
156	pub fn set_focus_color(&mut self, color: Option<Color>) {
157		self.focus_color = color;
158	}
159
160	fn value_changed_hook(&mut self) {
161		let old = self.base.constraints.x.max;
162		self.base.constraints.x.max = Size::Fixed(
163			NonZero::new(
164				self.radix.prefix_glyphs().len() as TSize
165					+ self
166						.value
167						.abs()
168						.checked_ilog(self.radix as i32)
169						.unwrap_or(0) as TSize
170					+ self.base.constraints.x.min.get()
171					+ self.value.is_negative() as TSize,
172			)
173			.unwrap(),
174		);
175		if old != self.base.constraints.x.max {
176			self.provoke_changed_property(WindowProperty::Size);
177		}
178	}
179}
180
181impl Window for Numeric {
182	fn render(&self, canvas: &mut Canvas) {
183		let len = self
184			.value
185			.abs()
186			.checked_ilog(self.radix as i32)
187			.unwrap_or(0) as TSize
188			+ 1;
189		let updown = 2;
190
191		let mut row = canvas.get_row(0, GlyphWidth::Half).unwrap();
192
193		row.get(row.length() - 1)
194			.unwrap()
195			.set_grapheme(Self::ARROW_UP)
196			.unwrap();
197		row.get(row.length() - 2)
198			.unwrap()
199			.set_grapheme(Self::ARROW_DOWN)
200			.unwrap();
201		if self.value == self.max_range {
202			row.get(row.length() - 1)
203				.unwrap()
204				.set_grapheme(Self::ARROW_UP_DISABLED)
205				.unwrap();
206		}
207		else if self.value == self.min_range {
208			row.get(row.length() - 2)
209				.unwrap()
210				.set_grapheme(Self::ARROW_DOWN_DISABLED)
211				.unwrap();
212		}
213
214		if row.length()
215			>= len
216				+ updown + self.value.is_negative() as TSize
217				+ self.radix.prefix_glyphs().len() as TSize
218		{
219			let focus_color = match self.has_focus {
220				true => self.focus_color,
221				false => None,
222			};
223			let mut start = 0;
224			if self.value < 0 {
225				let mut item = row.get(0).unwrap();
226				item.set_grapheme(Radix::NEGATIVE_INDICATOR).unwrap();
227				start += 1;
228				if let Some(color) = focus_color {
229					item.set_fg(color);
230				}
231			}
232			let end = row.length() - updown;
233
234			for g in self.radix.prefix_glyphs() {
235				let mut item = row.get(start).unwrap();
236				item.set_grapheme(*g).unwrap();
237				start += 1;
238			}
239
240			let mut rem = self.value.abs();
241			for idx in (start..end).rev() {
242				let mut item = row.get(idx).unwrap();
243				item.set_grapheme(self.radix.first_literal(rem as usize))
244					.unwrap();
245				rem /= self.radix as i32;
246			}
247		}
248		else {
249			let mut item = row.get(row.length() - 3).unwrap();
250			item.set_grapheme(Self::NUMERIC_PLACEHOLDER).unwrap();
251		}
252
253		if let Some(color) = self.focus_color
254			&& self.has_focus
255		{
256			row.fill_foreground(color);
257		}
258		else if let Some(color) = self.base.colors.base_fg {
259			row.fill_foreground(color);
260		}
261
262		if let Some(color) = self.base.colors.disabled
263			&& !self.base.enabled
264		{
265			row.fill_foreground(color);
266		}
267
268		if let Some(color) = self.base.colors.base_bg {
269			row.fill_background(color);
270		}
271	}
272
273	fn handle_event(&mut self, event: &mut WindowEvent) {
274		match event.raw() {
275			Event::FocusGained => self.has_focus = true,
276			Event::FocusLost => self.has_focus = false,
277			_ => (self.key_handler)(self, event),
278		}
279	}
280
281	fn is_enabled(&self) -> bool {
282		self.base.enabled
283	}
284}
285
286impl WindowLayout for Numeric {
287	fn desired_size(&self, available_size: TPoint) -> TPoint {
288		self.base.desired_size(available_size)
289	}
290
291	fn border(&self) -> BorderStyle {
292		self.base.border
293	}
294
295	fn alignment(&self) -> (HorizontalAlignment, VerticalAlignment) {
296		self.base.alignment
297	}
298
299	fn is_visible(&self) -> bool {
300		self.base.visibility
301	}
302
303	fn margin(&self) -> Thickness {
304		self.base.margin
305	}
306}
307
308impl HasWindowUID for Numeric {
309	fn uid(&self) -> WindowUID {
310		self.base.uid
311	}
312}
313
314impl Widget for Numeric {
315	fn set_visibility(&mut self, visibility: bool) {
316		self.base.visibility = visibility;
317		self.provoke_changed_property(WindowProperty::IsVisible);
318	}
319
320	/// Unsupported
321	fn set_width(&mut self, _: Size) {}
322
323	/// Unsupported
324	fn set_height(&mut self, _: Size) {}
325
326	fn set_margin(&mut self, margin: Thickness) {
327		self.base.margin = margin;
328		self.provoke_changed_property(WindowProperty::Margin);
329	}
330
331	fn set_border(&mut self, border: BorderStyle) {
332		self.base.border = border;
333		self.provoke_changed_property(WindowProperty::Border);
334	}
335
336	fn set_alignment(&mut self, horizontal: HorizontalAlignment, vertical: VerticalAlignment) {
337		self.base.alignment = (horizontal, vertical);
338		self.provoke_changed_property(WindowProperty::Alignment);
339	}
340
341	fn set_enabled_state(&mut self, is_enabled: bool) {
342		self.base.enabled = is_enabled;
343	}
344
345	/// Unsupported
346	fn set_width_constraint(&mut self, _: SizeConstraint) {}
347
348	/// Unsupported
349	fn set_height_constraint(&mut self, _: SizeConstraint) {}
350}
351
352impl WidgetColors for Numeric {
353	fn set_disabled_color(&mut self, color: Option<Color>) {
354		self.base.colors.disabled = color;
355	}
356
357	fn set_base_fg_color(&mut self, color: Option<Color>) {
358		self.base.colors.base_fg = color;
359	}
360
361	fn set_base_bg_color(&mut self, color: Option<Color>) {
362		self.base.colors.base_bg = color;
363	}
364}