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//! [`CmdLine`], 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 command_line::{
54 CmdLine, CmdLineCfg, CmdLineMode, IncSearch, PipeSelections, RunCommands, ShowNotifications,
55 },
56 file::{File, FileCfg, PathKind},
57 line_numbers::{LineNum, LineNumbers, LineNumbersCfg},
58 status_line::{State, StatusLine, StatusLineCfg, common, status},
59};
60use crate::{
61 cfg::PrintCfg,
62 context::FileParts,
63 data::{Data, RwData},
64 form,
65 hooks::{self, FocusedOn, UnfocusedFrom},
66 mode::Cursors,
67 text::Text,
68 ui::{Area, PushSpecs, Ui},
69};
70
71mod command_line;
72mod file;
73mod line_numbers;
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<(), duat_core::Error<()>> {
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<(), duat_core::Error<()>> {
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<(), duat_core::Error<()>> {
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<(), duat_core::Error<()>> {
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>: Send + Sync + 'static
313where
314 U: Ui,
315{
316 /// The configuration type
317 type Cfg: WidgetCfg<U, Widget = Self>
318 where
319 Self: Sized;
320
321 /// Returns a [`WidgetCfg`], for use in layout construction
322 ///
323 /// This function exists primarily so the [`WidgetCfg`]s
324 /// themselves don't need to be in scope. You will want to use
325 /// these in [hooks] like [`OnFileOpen`]:
326 ///
327 /// ```rust
328 /// # use duat_core::{
329 /// # hooks::{self, OnFileOpen},
330 /// # ui::{FileBuilder, Ui},
331 /// # widgets::{File, LineNumbers, Widget, common::selections_fmt, status},
332 /// # };
333 /// # fn test<U: Ui>() {
334 /// hooks::remove("FileWidgets");
335 /// hooks::add::<OnFileOpen<U>>(|builder| {
336 /// // Screw it, LineNumbers on both sides.
337 /// builder.push(LineNumbers::cfg());
338 /// builder.push(LineNumbers::cfg().on_the_right().align_right());
339 /// });
340 /// # }
341 /// ```
342 ///
343 /// [`OnFileOpen`]: crate::hooks::OnFileOpen
344 fn cfg() -> Self::Cfg
345 where
346 Self: Sized;
347
348 /// Updates the widget, allowing the modification of its [`Area`]
349 ///
350 /// There are a few contexts in which this function is triggered:
351 ///
352 /// * A key was sent to the widget, if it is an [`Widget`]
353 /// * It was modified externally by something like [`IncSearch`]
354 /// * The window was resized, so all widgets must be reprinted
355 ///
356 /// In this function, the text should be updated to match its new
357 /// conditions, or request changes to its [`Area`]. As an example,
358 /// the [`LineNumbers`] widget asks for [more or less width],
359 /// depending on the number of lines in the file, in order
360 /// to show an appropriate number of digits.
361 ///
362 /// [`Session`]: crate::session::Session
363 /// [more or less width]: Area::constrain_hor
364 fn update(&mut self, _area: &U::Area) {}
365
366 /// The text that this widget prints out
367 fn text(&self) -> &Text;
368
369 /// A mutable reference to the [`Text`] that is printed
370 fn text_mut(&mut self) -> &mut Text;
371
372 /// The [`Cursors`] that are used on the [`Text`], if they exist
373 fn cursors(&self) -> Option<&Cursors> {
374 self.text().cursors()
375 }
376
377 /// A mutable reference to the [`Cursors`], if they exist
378 fn cursors_mut(&mut self) -> Option<&mut Cursors> {
379 self.text_mut().cursors_mut()
380 }
381
382 /// Actions to do whenever this [`Widget`] is focused.
383 #[allow(unused)]
384 fn on_focus(&mut self, area: &U::Area) {}
385
386 /// Actions to do whenever this [`Widget`] is unfocused.
387 #[allow(unused)]
388 fn on_unfocus(&mut self, area: &U::Area) {}
389
390 /// The [configuration] for how to print [`Text`]
391 ///
392 /// The default configuration, used when `print_cfg` is not
393 /// implemented,can be found at [`PrintCfg::new`].
394 ///
395 /// [configuration]: PrintCfg
396 fn print_cfg(&self) -> PrintCfg {
397 PrintCfg::new()
398 }
399
400 /// Prints the widget
401 ///
402 /// Very rarely shouuld you actually implement this method, one
403 /// example of where this is actually implemented is in
404 /// [`File::print`], where [`Area::print_with`] is called in order
405 /// to simultaneously update the list of lines numbers, for
406 /// widgets like [`LineNumbers`] to read.
407 fn print(&mut self, area: &U::Area) {
408 let cfg = self.print_cfg();
409 area.print(self.text_mut(), cfg, form::painter())
410 }
411
412 /// Actions taken when this widget opens for the first time
413 ///
414 /// Examples of things that should go in here are [`form`]
415 /// functions, [hooks], [commands] you want executed only once
416 ///
417 /// [commands]: crate::cmd
418 fn once() -> Result<(), crate::Error<()>>
419 where
420 Self: Sized;
421}
422
423/// A configuration struct for a [`Widget`]
424///
425/// This configuration is used to make adjustments on how a widget
426/// will be added to a file or a window. These adjustments are
427/// primarily configurations for the widget itself, and to what
428/// direction it will be pushed:
429///
430/// ```rust
431/// # use duat_core::{
432/// # hooks::{self, OnFileOpen},
433/// # ui::Ui,
434/// # widgets::{LineNumbers, Widget},
435/// # };
436/// # fn test<U: Ui>() {
437/// hooks::add::<OnFileOpen<U>>(|builder| {
438/// // Change pushing direction to the right.
439/// let cfg = LineNumbers::cfg().on_the_right();
440/// // Changes to where the numbers will be placed.
441/// let cfg = cfg.align_right().align_main_left();
442///
443/// builder.push(cfg);
444/// });
445/// # }
446/// ```
447pub trait WidgetCfg<U>: Sized
448where
449 U: Ui,
450{
451 type Widget: Widget<U>;
452
453 fn build(self, on_file: bool) -> (Self::Widget, impl CheckerFn, PushSpecs);
454}
455
456// Elements related to the [`Widget`]s
457pub struct Node<U: Ui> {
458 widget: RwData<dyn Widget<U>>,
459 area: U::Area,
460
461 checker: Arc<dyn Fn() -> bool + Send + Sync>,
462 busy_updating: Arc<AtomicBool>,
463
464 related_widgets: Option<RwData<Vec<Node<U>>>>,
465 on_focus: fn(&Node<U>),
466 on_unfocus: fn(&Node<U>),
467}
468
469impl<U: Ui> Node<U> {
470 pub fn new<W: Widget<U>>(
471 widget: RwData<dyn Widget<U>>,
472 area: U::Area,
473 checker: impl CheckerFn,
474 ) -> Self {
475 fn related_widgets<U: Ui>(
476 widget: &RwData<dyn Widget<U>>,
477 area: &U::Area,
478 ) -> Option<RwData<Vec<Node<U>>>> {
479 widget.mutate_as(|f: &mut File| {
480 let cfg = f.print_cfg();
481 f.text_mut().add_cursors(area, cfg);
482 RwData::default()
483 })
484 }
485 let related_widgets = related_widgets::<U>(&widget, &area);
486
487 Self {
488 widget,
489 area,
490
491 checker: Arc::new(checker),
492 busy_updating: Arc::new(AtomicBool::new(false)),
493
494 related_widgets,
495 on_focus: Self::on_focus_fn::<W>,
496 on_unfocus: Self::on_unfocus_fn::<W>,
497 }
498 }
499
500 pub fn widget(&self) -> &RwData<dyn Widget<U>> {
501 &self.widget
502 }
503
504 /// Returns the downcast ref of this [`Widget`].
505 pub fn try_downcast<W>(&self) -> Option<RwData<W>> {
506 self.widget.try_downcast()
507 }
508
509 pub fn data_is<W: 'static>(&self) -> bool {
510 self.widget.data_is::<W>()
511 }
512
513 pub fn update_and_print(&self) {
514 self.busy_updating.store(true, Ordering::Release);
515
516 let mut widget = self.widget.raw_write();
517 widget.update(&self.area);
518 widget.print(&self.area);
519 drop(widget);
520
521 if let Some(nodes) = &self.related_widgets {
522 for node in &*nodes.read() {
523 if node.needs_update() {
524 node.update_and_print();
525 }
526 }
527 }
528
529 self.busy_updating.store(false, Ordering::Release);
530 }
531
532 pub fn inspect_as<W: 'static, B>(&self, f: impl FnOnce(&W) -> B) -> Option<B> {
533 self.widget.inspect_as(f)
534 }
535
536 pub fn ptr_eq<W, D>(&self, other: &D) -> bool
537 where
538 W: ?Sized,
539 D: Data<W> + ?Sized,
540 {
541 self.widget.ptr_eq(other)
542 }
543
544 pub fn needs_update(&self) -> bool {
545 if !self.busy_updating.load(Ordering::Acquire) {
546 (self.checker)() || self.area.has_changed()
547 } else {
548 false
549 }
550 }
551
552 pub(crate) fn update(&self) {
553 self.widget.raw_write().update(&self.area)
554 }
555
556 pub(crate) fn parts(&self) -> (&RwData<dyn Widget<U>>, &U::Area) {
557 (&self.widget, &self.area)
558 }
559
560 pub(crate) fn as_file(&self) -> Option<FileParts<U>> {
561 self.widget.try_downcast().map(|file| {
562 (
563 file,
564 self.area.clone(),
565 self.related_widgets.clone().unwrap(),
566 )
567 })
568 }
569
570 pub(crate) fn on_focus(&self) {
571 self.area.set_as_active();
572 (self.on_focus)(self)
573 }
574
575 pub(crate) fn on_unfocus(&self) {
576 (self.on_unfocus)(self)
577 }
578
579 pub(crate) fn raw_inspect<B>(&self, f: impl FnOnce(&dyn Widget<U>) -> B) -> B {
580 let widget = self.widget.raw_read();
581 f(&*widget)
582 }
583
584 pub(crate) fn area(&self) -> &U::Area {
585 &self.area
586 }
587
588 pub(crate) fn related_widgets(&self) -> Option<&RwData<Vec<Node<U>>>> {
589 self.related_widgets.as_ref()
590 }
591
592 fn on_focus_fn<W: Widget<U>>(&self) {
593 self.area.set_as_active();
594 let widget = self.widget.try_downcast().unwrap();
595 hooks::trigger::<FocusedOn<W, U>>((widget, self.area.clone()));
596 }
597
598 fn on_unfocus_fn<W: Widget<U>>(&self) {
599 let widget = self.widget.try_downcast().unwrap();
600 hooks::trigger::<UnfocusedFrom<W, U>>((widget, self.area.clone()));
601 }
602}
603
604impl<U: Ui> Clone for Node<U> {
605 fn clone(&self) -> Self {
606 Self {
607 widget: self.widget.clone(),
608 area: self.area.clone(),
609 checker: self.checker.clone(),
610 busy_updating: self.busy_updating.clone(),
611 related_widgets: self.related_widgets.clone(),
612 on_focus: self.on_focus,
613 on_unfocus: self.on_unfocus,
614 }
615 }
616}
617
618impl<U: Ui> PartialEq for Node<U> {
619 fn eq(&self, other: &Self) -> bool {
620 self.widget.ptr_eq(&other.widget)
621 }
622}
623
624impl<U: Ui> Eq for Node<U> {}
625
626pub trait CheckerFn = Fn() -> bool + 'static + Send + Sync;