duat_core/widgets/mod.rs
1//! APIs for the construction of widgets, and a few common ones.
2//!
3//! This module has the [`Widget`] trait, which is a region on the
4//! window containing a [`Text`], and may be modified by user mode
5//! (but not necessarily).
6//!
7//! With the exception of the [`File`], these widgets will show up in
8//! one of three contexts:
9//!
10//! - Being pushed to a [`File`] via the hook [`OnFileOpen`];
11//! - Being pushed to the outer edges via [`OnWindowOpen`];
12//! - Being pushed to popup widgets via `OnPopupOpen` (TODO);
13//!
14//! These widgets can be pushed to all 4 sides of other widgets,
15//! through the use of [`PushSpecs`]. When pushing widgets, you can
16//! also include [`Constraint`] in order to get a specific size on the
17//! screen for the widget.
18//!
19//! ```rust
20//! # use duat_core::ui::PushSpecs;
21//! let specs = PushSpecs::left().with_hor_min(10.0).with_ver_len(2.0);
22//! ```
23//!
24//! When pushing a widget with these `specs` to another widget, Duat
25//! will put it on the left, and _try_ to give it a minimum width of
26//! `10.0`, and a height of `2.0`.
27//!
28//! The module also provides 4 native widgets, [`File`] and
29//! [`PromptLine`], which can receive user mode, and [`StatusLine`]
30//! and [`LineNumbers`] which are not supposed to.
31//!
32//! These 4 widgets are supposed to be universal, not needing a
33//! specific [`Ui`] implementation to work. In contrast, you can
34//! create widgets for specific [`Ui`]s. As an example, the
35//! [`duat-term`] crate, which is a terminal [`Ui`] implementation for
36//! Duat, defines the [`VertRule`] widget, which is a separator that
37//! only makes sense in the context of a terminal.
38//!
39//! This module also describes a [`WidgetCfg`], which is used in
40//! widget construction.
41//!
42//! [`duat-term`]: https://docs.rs/duat-term/latest/duat_term/
43//! [`VertRule`]: https://docs.rs/duat-term/latest/duat_term/struct.VertRule.html
44//! [`OnFileOpen`]: crate::hooks::OnFileOpen
45//! [`OnWindowOpen`]: crate::hooks::OnWindowOpen
46//! [`Constraint`]: crate::ui::Constraint
47use std::sync::{
48 Arc,
49 atomic::{AtomicBool, Ordering},
50};
51
52pub use self::{
53 prompt_line::{PromptLine, PromptLineCfg},
54 file::{File, FileCfg, PathKind},
55 line_numbers::{LineNum, LineNumbers, LineNumbersOptions},
56 notifier::{Notifier, NotificationsCfg},
57 status_line::{State, StatusLine, StatusLineCfg, status},
58};
59use crate::{
60 cfg::PrintCfg,
61 context::FileParts,
62 data::{ReadDataGuard, RwData},
63 form,
64 hooks::{self, FocusedOn, UnfocusedFrom},
65 mode::Cursors,
66 text::Text,
67 ui::{Area, PushSpecs, Ui},
68};
69
70mod prompt_line;
71mod file;
72mod line_numbers;
73mod notifier;
74mod status_line;
75
76/// An area where [`Text`] will be printed to the screen
77///
78/// Most widgets are supposed to be passive widgets, that simply show
79/// information about the current state of Duat. If you want to see
80/// how to create a widget that takes in mode, see [`Mode`]
81///
82/// In order to show that information, widgets make use of [`Text`],
83/// which can show stylized text, buttons, and all sorts of other
84/// stuff.
85///
86/// For a demonstration on how to create a widget, I will create a
87/// widget that shows the uptime, in seconds, for Duat.
88///
89/// ```rust
90/// # use duat_core::text::Text;
91/// struct UpTime(Text);
92/// ```
93///
94/// In order to be a proper widget, it must have a [`Text`] to
95/// display. Next, I must implement [`Widget`]:
96///
97/// ```rust
98/// # use std::{marker::PhantomData, sync::OnceLock, time::{Duration, Instant}};
99/// # use duat_core::{
100/// # hooks, periodic_checker, text::Text, ui::{PushSpecs, Ui},
101/// # widgets::{Widget, WidgetCfg},
102/// # };
103/// # struct UpTime(Text);
104/// # struct UpTimeCfg<U>(PhantomData<U>);
105/// # impl<U: Ui> WidgetCfg<U> for UpTimeCfg<U> {
106/// # type Widget = UpTime;
107/// # fn build(self,_: bool) -> (Self::Widget, impl Fn() -> bool + 'static, PushSpecs) {
108/// # (UpTime(Text::new()), || false, PushSpecs::below())
109/// # }
110/// # }
111/// impl<U: Ui> Widget<U> for UpTime {
112/// type Cfg = UpTimeCfg<U>;
113///
114/// fn cfg() -> Self::Cfg {
115/// UpTimeCfg(PhantomData)
116/// }
117/// // ...
118/// # fn text(&self) -> &Text {
119/// # &self.0
120/// # }
121/// # fn text_mut(&mut self) -> &mut Text {
122/// # &mut self.0
123/// # }
124/// # fn once() -> Result<(), Text> {
125/// # Ok(())
126/// # }
127/// }
128/// ```
129///
130/// Note the [`Cfg`](Widget::Cfg) type, and the [`cfg`] method.
131/// These exist to give the user the ability to modify the widgets
132/// before they are pushed. The `Cfg` type, which implements
133/// [`WidgetCfg`] is the thing that will actually construct the
134/// widget. Let's look at `UpTimeCfg`:
135///
136/// ```rust
137/// # use std::{marker::PhantomData, sync::OnceLock, time::{Duration, Instant}};
138/// # use duat_core::{
139/// # hooks, periodic_checker, text::Text, ui::{PushSpecs, Ui}, widgets::{Widget, WidgetCfg},
140/// # };
141/// # struct UpTime(Text);
142/// struct UpTimeCfg<U>(PhantomData<U>);
143///
144/// impl<U: Ui> WidgetCfg<U> for UpTimeCfg<U> {
145/// type Widget = UpTime;
146///
147/// fn build(self, on_file: bool) -> (UpTime, impl Fn() -> bool + 'static, PushSpecs) {
148/// let widget = UpTime(Text::new());
149/// let checker = periodic_checker(Duration::new(1, 0));
150/// let specs = PushSpecs::below().with_ver_len(1.0);
151///
152/// (widget, checker, specs)
153/// }
154/// }
155/// # impl<U: Ui> Widget<U> for UpTime {
156/// # type Cfg = UpTimeCfg<U>;
157/// # fn cfg() -> Self::Cfg {
158/// # UpTimeCfg(PhantomData)
159/// # }
160/// # fn text(&self) -> &Text {
161/// # &self.0
162/// # }
163/// # fn text_mut(&mut self) -> &mut Text{
164/// # &mut self.0
165/// # }
166/// # fn once() -> Result<(), Text> {
167/// # Ok(())
168/// # }
169/// # }
170/// ```
171///
172/// The [`build`] method should return 3 objects:
173///
174/// * The widget itself.
175/// * A checker function that tells Duat when to update the widget.
176/// * [How] to push the widget into the [`File`]/window.
177///
178/// In this case, [`periodic_checker`] returns a function that returns
179/// `true` every `duration` that passes.
180///
181/// Also, note that `UpTimeCfg` includes a [`PhantomData<U>`]. This is
182/// done so that the end user does not need to specify a [`Ui`] when
183/// using [`WidgetCfg`]s.
184///
185/// Now, there are some other methods from [`Widget`] that need
186/// to be implemented for this to work. First of all, there needs to
187/// be a starting [`Instant`] to compare with the current moment in
188/// time.
189///
190/// The best time to do something like this is after Duat is done with
191/// initial setup. This happens when the [`ConfigLoaded`] hook is
192/// triggered.
193///
194/// ```rust
195/// # use std::{sync::OnceLock, time::Instant};
196/// # use duat_core::{hooks::{self, ConfigLoaded}, ui::Ui};
197/// # fn test<U: Ui>() {
198/// static START_TIME: OnceLock<Instant> = OnceLock::new();
199/// hooks::add::<ConfigLoaded>(|_| START_TIME.set(Instant::now()).unwrap());
200/// # }
201/// ```
202///
203/// I could put this code inside the [`cfg`] method, however, by
204/// doing so, it will be called every time this widget is added to the
205/// ui.
206///
207/// Instead, I'll put it in [`Widget::once`]. This function is
208/// only triggered once, no matter how many times the widget is added
209/// to the ui:
210///
211/// ```rust
212/// # use std::{marker::PhantomData, sync::OnceLock, time::{Duration, Instant}};
213/// # use duat_core::{
214/// # form::{self, Form}, hooks::{self, ConfigLoaded}, periodic_checker,
215/// # text::Text, ui::{PushSpecs, Ui}, widgets::{Widget, WidgetCfg},
216/// # };
217/// # struct UpTime(Text);
218/// # struct UpTimeCfg<U>(PhantomData<U>);
219/// # impl<U: Ui> WidgetCfg<U> for UpTimeCfg<U> {
220/// # type Widget = UpTime;
221/// # fn build(self, on_file: bool) -> (UpTime, impl Fn() -> bool + 'static, PushSpecs) {
222/// # (UpTime(Text::new()), || false, PushSpecs::below())
223/// # }
224/// # }
225/// static START_TIME: OnceLock<Instant> = OnceLock::new();
226///
227/// impl<U: Ui> Widget<U> for UpTime {
228/// # type Cfg = UpTimeCfg<U>;
229/// # fn cfg() -> Self::Cfg {
230/// # UpTimeCfg(PhantomData)
231/// # }
232/// # fn text(&self) -> &Text {
233/// # &self.0
234/// # }
235/// # fn text_mut(&mut self) -> &mut Text {
236/// # &mut self.0
237/// # }
238/// // ...
239/// fn once() -> Result<(), Text> {
240/// hooks::add::<ConfigLoaded>(|_| {
241/// START_TIME.set(Instant::now()).unwrap()
242/// });
243/// form::set_weak("UpTime", Form::cyan());
244/// Ok(())
245/// }
246/// }
247/// ```
248///
249/// I also added the `"UpTime"` [`Form`], which will be used by the
250/// widget when it is updated. When adding form, you should use the
251/// [`form::set_weak*`] functions, in order to not interfere with
252/// the configuration crate.
253///
254/// Next, I need to implement the [`update`] method, which will simply
255/// format the [`Text`] into a readable format:
256///
257/// ```rust
258/// # use std::{marker::PhantomData, sync::OnceLock, time::{Duration, Instant}};
259/// # use duat_core::{
260/// # hooks, periodic_checker, text::{Text, text}, ui::{PushSpecs, Ui},
261/// # widgets::{Widget, WidgetCfg},
262/// # };
263/// # struct UpTime(Text);
264/// # struct UpTimeCfg<U>(PhantomData<U>);
265/// # impl<U: Ui> WidgetCfg<U> for UpTimeCfg<U> {
266/// # type Widget = UpTime;
267/// # fn build(self, on_file: bool) -> (UpTime, impl Fn() -> bool + 'static, PushSpecs) {
268/// # (UpTime(Text::new()), || false, PushSpecs::below())
269/// # }
270/// # }
271/// # static START_TIME: OnceLock<Instant> = OnceLock::new();
272/// impl<U: Ui> Widget<U> for UpTime {
273/// # type Cfg = UpTimeCfg<U>;
274/// # fn cfg() -> Self::Cfg {
275/// # UpTimeCfg(PhantomData)
276/// # }
277/// # fn text(&self) -> &Text {
278/// # &self.0
279/// # }
280/// # fn text_mut(&mut self) -> &mut Text {
281/// # &mut self.0
282/// # }
283/// // ...
284/// fn update(&mut self, _area: &U::Area) {
285/// let Some(start) = START_TIME.get() else {
286/// return;
287/// };
288/// let duration = start.elapsed();
289/// let mins = duration.as_secs() / 60;
290/// let secs = duration.as_secs() % 60;
291/// self.0 = text!([UpTime] mins "m " secs "s");
292/// }
293/// // ...
294/// fn once() -> Result<(), Text> {
295/// Ok(())
296/// }
297/// }
298/// ```
299///
300/// [`Mode`]: crate::mode::Mode
301/// [`cfg`]: Widget::cfg
302/// [`build`]: WidgetCfg::build
303/// [How]: PushSpecs
304/// [`periodic_checker`]: crate::periodic_checker
305/// [`PhantomData<U>`]: std::marker::PhantomData
306/// [`Instant`]: std::time::Instant
307/// [`ConfigLoaded`]: crate::hooks::ConfigLoaded
308/// [`update`]: Widget::update
309/// [`Form`]: crate::form::Form
310/// [`form::set_weak*`]: crate::form::set_weak
311/// [`text!`]: crate::text::text
312pub trait Widget<U: Ui>: Send + 'static {
313 /// The configuration type
314 type Cfg: WidgetCfg<U, Widget = Self>
315 where
316 Self: Sized;
317
318 /// Returns a [`WidgetCfg`], for use in layout construction
319 ///
320 /// This function exists primarily so the [`WidgetCfg`]s
321 /// themselves don't need to be in scope. You will want to use
322 /// these in [hooks] like [`OnFileOpen`]:
323 ///
324 /// ```rust
325 /// # use duat_core::{
326 /// # hooks::{self, OnFileOpen}, ui::{FileBuilder, Ui},
327 /// # widgets::{File, LineNumbers, Widget},
328 /// # };
329 /// # fn test<U: Ui>() {
330 /// hooks::remove("FileWidgets");
331 /// hooks::add::<OnFileOpen<U>>(|builder| {
332 /// // Screw it, LineNumbers on both sides.
333 /// builder.push(LineNumbers::cfg());
334 /// builder.push(LineNumbers::cfg().on_the_right().align_right());
335 /// });
336 /// # }
337 /// ```
338 ///
339 /// [`OnFileOpen`]: crate::hooks::OnFileOpen
340 fn cfg() -> Self::Cfg
341 where
342 Self: Sized;
343
344 /// Updates the widget, allowing the modification of its [`Area`]
345 ///
346 /// There are a few contexts in which this function is triggered:
347 ///
348 /// * A key was sent to the widget, if it is an [`Widget`]
349 /// * It was modified externally by something like [`IncSearch`]
350 /// * The window was resized, so all widgets must be reprinted
351 ///
352 /// In this function, the text should be updated to match its new
353 /// conditions, or request changes to its [`Area`]. As an example,
354 /// the [`LineNumbers`] widget asks for [more or less width],
355 /// depending on the number of lines in the file, in order
356 /// to show an appropriate number of digits.
357 ///
358 /// [`Session`]: crate::session::Session
359 /// [more or less width]: Area::constrain_hor
360 /// [`IncSearch`]: crate::mode::IncSearch
361 fn update(&mut self, _area: &U::Area) {}
362
363 /// The text that this widget prints out
364 fn text(&self) -> &Text;
365
366 /// A mutable reference to the [`Text`] that is printed
367 fn text_mut(&mut self) -> &mut Text;
368
369 /// The [`Cursors`] that are used on the [`Text`], if they exist
370 fn cursors(&self) -> Option<&Cursors> {
371 self.text().cursors()
372 }
373
374 /// A mutable reference to the [`Cursors`], if they exist
375 fn cursors_mut(&mut self) -> Option<&mut Cursors> {
376 self.text_mut().cursors_mut()
377 }
378
379 /// Actions to do whenever this [`Widget`] is focused.
380 #[allow(unused)]
381 fn on_focus(&mut self, area: &U::Area) {}
382
383 /// Actions to do whenever this [`Widget`] is unfocused.
384 #[allow(unused)]
385 fn on_unfocus(&mut self, area: &U::Area) {}
386
387 /// The [configuration] for how to print [`Text`]
388 ///
389 /// The default configuration, used when `print_cfg` is not
390 /// implemented,can be found at [`PrintCfg::new`].
391 ///
392 /// [configuration]: PrintCfg
393 fn print_cfg(&self) -> PrintCfg {
394 PrintCfg::new()
395 }
396
397 /// Prints the widget
398 ///
399 /// Very rarely shouuld you actually implement this method, one
400 /// example of where this is actually implemented is in
401 /// [`File::print`], where [`Area::print_with`] is called in order
402 /// to simultaneously update the list of lines numbers, for
403 /// widgets like [`LineNumbers`] to read.
404 fn print(&mut self, area: &U::Area) {
405 // crate::log_file!("printing {}", crate::duat_name::<Self>());
406 let cfg = self.print_cfg();
407 area.print(self.text_mut(), cfg, form::painter::<Self>())
408 }
409
410 /// Actions taken when this widget opens for the first time
411 ///
412 /// Examples of things that should go in here are [`form`]
413 /// functions, [hooks], [commands] you want executed only once
414 ///
415 /// [commands]: crate::cmd
416 fn once() -> Result<(), Text>
417 where
418 Self: Sized;
419}
420
421/// A configuration struct for a [`Widget`]
422///
423/// This configuration is used to make adjustments on how a widget
424/// will be added to a file or a window. These adjustments are
425/// primarily configurations for the widget itself, and to what
426/// direction it will be pushed:
427///
428/// ```rust
429/// # use duat_core::{
430/// # hooks::{self, OnFileOpen},
431/// # ui::Ui,
432/// # widgets::{LineNumbers, Widget},
433/// # };
434/// # fn test<U: Ui>() {
435/// hooks::add::<OnFileOpen<U>>(|builder| {
436/// // Change pushing direction to the right.
437/// let cfg = LineNumbers::cfg().on_the_right();
438/// // Changes to where the numbers will be placed.
439/// let cfg = cfg.align_right().align_main_left();
440///
441/// builder.push(cfg);
442/// });
443/// # }
444/// ```
445pub trait WidgetCfg<U>: Sized
446where
447 U: Ui,
448{
449 type Widget: Widget<U>;
450
451 fn build(self, on_file: bool) -> (Self::Widget, impl CheckerFn, PushSpecs);
452}
453
454// Elements related to the [`Widget`]s
455#[derive(Clone)]
456pub struct Node<U: Ui> {
457 widget: RwData<dyn Widget<U>>,
458 area: U::Area,
459
460 checker: Arc<dyn Fn() -> bool + Send + Sync>,
461 busy_updating: Arc<AtomicBool>,
462
463 related_widgets: Related<U>,
464 on_focus: fn(&Node<U>),
465 on_unfocus: fn(&Node<U>),
466}
467
468impl<U: Ui> Node<U> {
469 pub fn new<W: Widget<U>>(
470 widget: RwData<dyn Widget<U>>,
471 area: U::Area,
472 checker: impl CheckerFn,
473 ) -> Self {
474 fn related_widgets<U: Ui>(
475 widget: &RwData<dyn Widget<U>>,
476 area: &U::Area,
477 ) -> Option<RwData<Vec<Node<U>>>> {
478 widget.write_as::<File>().map(|mut file| {
479 let cfg = file.print_cfg();
480 file.text_mut().add_cursors(area, cfg);
481 RwData::default()
482 })
483 }
484 let related_widgets = related_widgets::<U>(&widget, &area);
485
486 Self {
487 widget,
488 area,
489
490 checker: Arc::new(checker),
491 busy_updating: Arc::new(AtomicBool::new(false)),
492
493 related_widgets,
494 on_focus: Self::on_focus_fn::<W>,
495 on_unfocus: Self::on_unfocus_fn::<W>,
496 }
497 }
498
499 pub fn widget(&self) -> &RwData<dyn Widget<U>> {
500 &self.widget
501 }
502
503 /// Returns the downcast ref of this [`Widget`].
504 pub fn try_downcast<W>(&self) -> Option<RwData<W>> {
505 self.widget.try_downcast()
506 }
507
508 pub fn data_is<W: 'static>(&self) -> bool {
509 self.widget.data_is::<W>()
510 }
511
512 pub fn update_and_print(&self) {
513 self.busy_updating.store(true, Ordering::Release);
514
515 if let Some(nodes) = &self.related_widgets {
516 for node in &*nodes.read() {
517 if node.needs_update() {
518 node.update_and_print();
519 }
520 }
521 }
522
523 let mut widget = self.widget.raw_write();
524
525 let cfg = widget.print_cfg();
526 widget.text_mut().remove_cursors(&self.area, cfg);
527
528 widget.update(&self.area);
529
530 widget.text_mut().add_cursors(&self.area, cfg);
531 if let Some(main) = widget.cursors().and_then(Cursors::get_main) {
532 self.area
533 .scroll_around_point(widget.text(), main.caret(), widget.print_cfg());
534 }
535
536 widget.print(&self.area);
537 drop(widget);
538
539 self.busy_updating.store(false, Ordering::Release);
540 }
541
542 pub fn read_as<W: 'static>(&self) -> Option<ReadDataGuard<'_, W>> {
543 self.widget.read_as()
544 }
545
546 pub fn ptr_eq<W: ?Sized>(&self, other: &RwData<W>) -> bool {
547 self.widget.ptr_eq(other)
548 }
549
550 pub fn needs_update(&self) -> bool {
551 if !self.busy_updating.load(Ordering::Acquire) {
552 self.area.has_changed() || (self.checker)()
553 } else {
554 false
555 }
556 }
557
558 pub(crate) fn update(&self) {
559 self.widget.raw_write().update(&self.area)
560 }
561
562 pub(crate) fn parts(&self) -> (&RwData<dyn Widget<U>>, &<U as Ui>::Area, &Related<U>) {
563 (&self.widget, &self.area, &self.related_widgets)
564 }
565
566 pub(crate) fn as_file(&self) -> Option<FileParts<U>> {
567 self.widget.try_downcast().map(|file| {
568 (
569 file,
570 self.area.clone(),
571 self.related_widgets.clone().unwrap(),
572 )
573 })
574 }
575
576 pub(crate) fn on_focus(&self) {
577 self.area.set_as_active();
578 (self.on_focus)(self)
579 }
580
581 pub(crate) fn on_unfocus(&self) {
582 (self.on_unfocus)(self)
583 }
584
585 pub(crate) fn raw_inspect<B>(&self, f: impl FnOnce(&dyn Widget<U>) -> B) -> B {
586 let widget = self.widget.raw_read();
587 f(&*widget)
588 }
589
590 pub(crate) fn area(&self) -> &U::Area {
591 &self.area
592 }
593
594 pub(crate) fn related_widgets(&self) -> Option<&RwData<Vec<Node<U>>>> {
595 self.related_widgets.as_ref()
596 }
597
598 fn on_focus_fn<W: Widget<U>>(&self) {
599 self.area.set_as_active();
600 let widget = self.widget.try_downcast().unwrap();
601 hooks::trigger::<FocusedOn<W, U>>((widget, self.area.clone()));
602 }
603
604 fn on_unfocus_fn<W: Widget<U>>(&self) {
605 let widget = self.widget.try_downcast().unwrap();
606 hooks::trigger::<UnfocusedFrom<W, U>>((widget, self.area.clone()));
607 }
608}
609
610impl<U: Ui> PartialEq for Node<U> {
611 fn eq(&self, other: &Self) -> bool {
612 self.widget.ptr_eq(&other.widget)
613 }
614}
615
616impl<U: Ui> Eq for Node<U> {}
617
618pub trait CheckerFn = Fn() -> bool + Send + Sync + 'static;
619
620pub type Related<U> = Option<RwData<Vec<Node<U>>>>;