duat_core/widgets/
mod.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
//! APIs for the construction of widgets, and a few common ones.
//!
//! This module has the [`Widget`] trait, which is a region on the
//! window containing a [`Text`], and may be modified by user mode
//! (but not necessarily).
//!
//! With the exception of the [`File`], these widgets will show up in
//! one of three contexts:
//!
//! - Being pushed to a [`File`] via the hook [`OnFileOpen`];
//! - Being pushed to the outer edges via [`OnWindowOpen`];
//! - Being pushed to popup widgets via `OnPopupOpen` (TODO);
//!
//! These widgets can be pushed to all 4 sides of other widgets,
//! through the use of [`PushSpecs`]. When pushing widgets, you can
//! also include [`Constraint`] in order to get a specific size on the
//! screen for the widget.
//!
//! ```rust
//! # use duat_core::ui::PushSpecs;
//! let specs = PushSpecs::left().with_hor_min(10.0).with_ver_len(2.0);
//! ```
//!
//! When pushing a widget with these `specs` to another widget, Duat
//! will put it on the left, and _try_ to give it a minimum width of
//! `10.0`, and a height of `2.0`.
//!
//! The module also provides 4 native widgets, [`File`] and
//! [`CmdLine`], which can receive user mode, and [`StatusLine`]
//! and [`LineNumbers`] which are not supposed to.
//!
//! These 4 widgets are supposed to be universal, not needing a
//! specific [`Ui`] implementation to work. In contrast, you can
//! create widgets for specific [`Ui`]s. As an example, the
//! [`duat-term`] crate, which is a terminal [`Ui`] implementation for
//! Duat, defines the [`VertRule`] widget, which is a separator that
//! only makes sense in the context of a terminal.
//!
//! This module also describes a [`WidgetCfg`], which is used in
//! widget construction.
//!
//! [`duat-term`]: https://docs.rs/duat-term/latest/duat_term/
//! [`VertRule`]: https://docs.rs/duat-term/latest/duat_term/struct.VertRule.html
//! [`OnFileOpen`]: crate::hooks::OnFileOpen
//! [`OnWindowOpen`]: crate::hooks::OnWindowOpen
//! [`Constraint`]: crate::ui::Constraint
use std::sync::{
    Arc,
    atomic::{AtomicBool, Ordering},
};

pub use self::{
    command_line::{CmdLine, CmdLineCfg, CmdLineMode, IncSearch, RunCommands, ShowNotifications},
    file::{File, FileCfg},
    line_numbers::{LineNumbers, LineNumbersCfg},
    status_line::{State, StatusLine, StatusLineCfg, common, status},
};
use crate::{
    cfg::PrintCfg,
    context::FileParts,
    data::{Data, RwData},
    forms,
    hooks::{self, FocusedOn, UnfocusedFrom},
    mode::Cursors,
    text::Text,
    ui::{Area, PushSpecs, Ui},
};

mod command_line;
mod file;
mod line_numbers;
mod status_line;

/// An area where [`Text`] will be printed to the screen
///
/// Most widgets are supposed to be passive widgets, that simply show
/// information about the current state of Duat. If you want to see
/// how to create a widget that takes in mode, see [`Mode`]
///
/// In order to show that information, widgets make use of [`Text`],
/// which can show stylized text, buttons, and all sorts of other
/// stuff.
///
/// For a demonstration on how to create a widget, I will create a
/// widget that shows the uptime, in seconds, for Duat.
///
/// ```rust
/// # use duat_core::text::Text;
/// struct UpTime(Text);
/// ```
///
/// In order to be a proper widget, it must have a [`Text`] to
/// display. Next, I must implement [`Widget`]:
///
/// ```rust
/// # use std::{marker::PhantomData, sync::OnceLock, time::{Duration, Instant}};
/// # use duat_core::{
/// #     hooks, periodic_checker, text::Text, ui::{PushSpecs, Ui},
/// #     widgets::{Widget, WidgetCfg},
/// # };
/// # struct UpTime(Text);
/// # struct UpTimeCfg<U>(PhantomData<U>);
/// # impl<U: Ui> WidgetCfg<U> for UpTimeCfg<U> {
/// #     type Widget = UpTime;
/// #     fn build(self,_: bool) -> (Self::Widget, impl Fn() -> bool + 'static, PushSpecs) {
/// #         (UpTime(Text::new()), || false, PushSpecs::below())
/// #     }
/// # }
/// impl<U: Ui> Widget<U> for UpTime {
///     type Cfg = UpTimeCfg<U>;
///
///     fn cfg() -> Self::Cfg {
///         UpTimeCfg(PhantomData)
///     }
///     // ...
/// #     fn text(&self) -> &Text {
/// #         &self.0
/// #     }
/// #     fn text_mut(&mut self) -> &mut Text {
/// #        &mut self.0
/// #     }
/// #     fn once() {}
/// }
/// ```
///
/// Note the [`Cfg`](Widget::Cfg) type, and the [`cfg`] method.
/// These exist to give the user the ability to modify the widgets
/// before they are pushed. The `Cfg` type, which implements
/// [`WidgetCfg`] is the thing that will actually construct the
/// widget. Let's look at `UpTimeCfg`:
///
/// ```rust
/// # use std::{marker::PhantomData, sync::OnceLock, time::{Duration, Instant}};
/// # use duat_core::{
/// #     hooks, periodic_checker, text::Text, ui::{PushSpecs, Ui}, widgets::{Widget, WidgetCfg},
/// # };
/// # struct UpTime(Text);
/// struct UpTimeCfg<U>(PhantomData<U>);
///
/// impl<U: Ui> WidgetCfg<U> for UpTimeCfg<U> {
///     type Widget = UpTime;
///
///     fn build(self, on_file: bool) -> (UpTime, impl Fn() -> bool + 'static, PushSpecs) {
///         let widget = UpTime(Text::new());
///         let checker = periodic_checker(Duration::new(1, 0));
///         let specs = PushSpecs::below().with_ver_len(1.0);
///
///         (widget, checker, specs)
///     }
/// }
/// # impl<U: Ui> Widget<U> for UpTime {
/// #     type Cfg = UpTimeCfg<U>;
/// #     fn cfg() -> Self::Cfg {
/// #         UpTimeCfg(PhantomData)
/// #     }
/// #     fn text(&self) -> &Text {
/// #         &self.0
/// #     }
/// #     fn text_mut(&mut self) -> &mut Text{
/// #         &mut self.0
/// #     }
/// #     fn once() {}
/// # }
/// ```
///
/// The [`build`] method should return 3 objects:
///
/// * The widget itself.
/// * A checker function that tells Duat when to update the widget.
/// * [How] to push the widget into the [`File`]/window.
///
/// In this case, [`periodic_checker`] returns a function that returns
/// `true` every `duration` that passes.
///
/// Also, note that `UpTimeCfg` includes a [`PhantomData<U>`]. This is
/// done so that the end user does not need to specify a [`Ui`] when
/// using [`WidgetCfg`]s.
///
/// Now, there are some other methods from [`Widget`] that need
/// to be implemented for this to work. First of all, there needs to
/// be a starting [`Instant`] to compare with the current moment in
/// time.
///
/// The best time to do something like this is after Duat is done with
/// initial setup. This happens when the [`SessionStarted`] hook is
/// triggered.
///
/// ```rust
/// # use std::{sync::OnceLock, time::Instant};
/// # use duat_core::{hooks::{self, SessionStarted}, ui::Ui};
/// # fn test<U: Ui>() {
/// static START_TIME: OnceLock<Instant> = OnceLock::new();
/// hooks::add::<SessionStarted<U>>(|_context| {
///     START_TIME.set(Instant::now()).unwrap();
/// });
/// # }
/// ```
///
/// I could put this code inside the [`cfg`] method, however, by
/// doing so, it will be called every time this widget is added to the
/// ui.
///
/// Instead, I'll put it in [`Widget::once`]. This function is
/// only triggered once, no matter how many times the widget is added
/// to the ui:
///
/// ```rust
/// # use std::{marker::PhantomData, sync::OnceLock, time::{Duration, Instant}};
/// # use duat_core::{
/// #     forms::{self, Form}, hooks::{self, SessionStarted}, periodic_checker,
/// #     text::Text, ui::{PushSpecs, Ui}, widgets::{Widget, WidgetCfg},
/// # };
/// # struct UpTime(Text);
/// # struct UpTimeCfg<U>(PhantomData<U>);
/// # impl<U: Ui> WidgetCfg<U> for UpTimeCfg<U> {
/// #     type Widget = UpTime;
/// #     fn build(self, on_file: bool) -> (UpTime, impl Fn() -> bool + 'static, PushSpecs) {
/// #         (UpTime(Text::new()), || false, PushSpecs::below())
/// #     }
/// # }
/// static START_TIME: OnceLock<Instant> = OnceLock::new();
///
/// impl<U: Ui> Widget<U> for UpTime {
/// #     type Cfg = UpTimeCfg<U>;
/// #     fn cfg() -> Self::Cfg {
/// #         UpTimeCfg(PhantomData)
/// #     }
/// #     fn text(&self) -> &Text {
/// #         &self.0
/// #     }
/// #     fn text_mut(&mut self) -> &mut Text {
/// #         &mut self.0
/// #     }
///     // ...
///     fn once() {
///         hooks::add::<SessionStarted<U>>(|_| {
///             START_TIME.set(Instant::now()).unwrap();
///         });
///         forms::set_weak("UpTime", Form::cyan());
///     }
/// }
/// ```
///
/// I also added the `"UpTime"` [`Form`], which will be used by the
/// widget when it is updated. When adding forms, you should use the
/// [`forms::set_weak*`] functions, in order to not interfere with
/// the configuration crate.
///
/// Next, I need to implement the [`update`] method, which will simply
/// format the [`Text`] into a readable format:
///
/// ```rust
/// # use std::{marker::PhantomData, sync::OnceLock, time::{Duration, Instant}};
/// # use duat_core::{
/// #     hooks, periodic_checker, text::{Text, text}, ui::{PushSpecs, Ui},
/// #     widgets::{Widget, WidgetCfg},
/// # };
/// # struct UpTime(Text);
/// # struct UpTimeCfg<U>(PhantomData<U>);
/// # impl<U: Ui> WidgetCfg<U> for UpTimeCfg<U> {
/// #     type Widget = UpTime;
/// #     fn build(self, on_file: bool) -> (UpTime, impl Fn() -> bool + 'static, PushSpecs) {
/// #         (UpTime(Text::new()), || false, PushSpecs::below())
/// #     }
/// # }
/// # static START_TIME: OnceLock<Instant> = OnceLock::new();
/// impl<U: Ui> Widget<U> for UpTime {
/// #     type Cfg = UpTimeCfg<U>;
/// #     fn cfg() -> Self::Cfg {
/// #         UpTimeCfg(PhantomData)
/// #     }
/// #     fn text(&self) -> &Text {
/// #         &self.0
/// #     }
/// #     fn text_mut(&mut self) -> &mut Text {
/// #         &mut self.0
/// #     }
///     // ...
///     fn update(&mut self, _area: &U::Area) {
///         let Some(start) = START_TIME.get() else {
///             return;
///         };
///         let duration = start.elapsed();
///         let mins = duration.as_secs() / 60;
///         let secs = duration.as_secs() % 60;
///         self.0 = text!([UpTime] mins "m " secs "s");
///     }
///     // ...
///     fn once() {}
/// }
/// ```
///
/// [`Mode`]: crate::mode::Mode
/// [`cfg`]: Widget::cfg
/// [`build`]: WidgetCfg::build
/// [How]: PushSpecs
/// [`periodic_checker`]: crate::periodic_checker
/// [`PhantomData<U>`]: std::marker::PhantomData
/// [`Instant`]: std::time::Instant
/// [`SessionStarted`]: crate::hooks::SessionStarted
/// [`update`]: Widget::update
/// [`Form`]: crate::forms::Form
/// [`forms::set_weak*`]: crate::forms::set_weak
/// [`text!`]: crate::text::text
pub trait Widget<U>: Send + Sync + 'static
where
    U: Ui,
{
    /// The configuration type
    type Cfg: WidgetCfg<U, Widget = Self>
    where
        Self: Sized;

    /// Returns a [`WidgetCfg`], for use in layout construction
    ///
    /// This function exists primarily so the [`WidgetCfg`]s
    /// themselves don't need to be in scope. You will want to use
    /// these in [hooks] like [`OnFileOpen`]:
    ///
    /// ```rust
    /// # use duat_core::{
    /// #     hooks::{self, OnFileOpen},
    /// #     ui::{FileBuilder, Ui},
    /// #     widgets::{File, LineNumbers, Widget, common::selections_fmt, status},
    /// # };
    /// # fn test<U: Ui>() {
    /// hooks::remove("FileWidgets");
    /// hooks::add::<OnFileOpen<U>>(|builder| {
    ///     // Screw it, LineNumbers on both sides.
    ///     builder.push(LineNumbers::cfg());
    ///     builder.push(LineNumbers::cfg().on_the_right().align_right());
    /// });
    /// # }
    /// ```
    ///
    /// [`OnFileOpen`]: crate::hooks::OnFileOpen
    fn cfg() -> Self::Cfg
    where
        Self: Sized;

    /// Updates the widget, allowing the modification of its [`Area`]
    ///
    /// There are a few contexts in which this function is triggered:
    ///
    /// * A key was sent to the widget, if it is an [`Widget`]
    /// * It was modified externally by something like [`IncSearch`]
    /// * The window was resized, so all widgets must be reprinted
    ///
    /// In this function, the text should be updated to match its new
    /// conditions, or request changes to its [`Area`]. As an example,
    /// the [`LineNumbers`] widget asks for [more or less width],
    /// depending on the number of lines in the file, in order
    /// to show an appropriate number of digits.
    ///
    /// [`Session`]: crate::session::Session
    /// [more or less width]: Area::constrain_hor
    fn update(&mut self, _area: &U::Area) {}

    /// The text that this widget prints out
    fn text(&self) -> &Text;

    /// Returns the [`&mut Text`] that is printed
    ///
    /// [`&mut Text`]: Text
    fn text_mut(&mut self) -> &mut Text;

    /// Actions to do whenever this [`Widget`] is focused.
    #[allow(unused)]
    fn on_focus(&mut self, area: &U::Area) {}

    /// Actions to do whenever this [`Widget`] is unfocused.
    #[allow(unused)]
    fn on_unfocus(&mut self, area: &U::Area) {}

    /// The [configuration] for how to print [`Text`]
    ///
    /// The default configuration, used when `print_cfg` is not
    /// implemented,can be found at [`PrintCfg::new`].
    ///
    /// [configuration]: PrintCfg
    fn print_cfg(&self) -> PrintCfg {
        PrintCfg::new()
    }

    /// Prints the widget
    ///
    /// Very rarely shouuld you actually implement this method, one
    /// example of where this is actually implemented is in
    /// [`File::print`], where [`Area::print_with`] is called in order
    /// to simultaneously update the list of lines numbers, for
    /// widgets like [`LineNumbers`] to read.
    fn print(&mut self, area: &U::Area) {
        area.print(self.text(), self.print_cfg(), forms::painter())
    }

    /// Actions taken when this widget opens for the first time
    ///
    /// Examples of things that should go in here are [`forms`]
    /// functions, [hooks], [commands] you want executed only once
    ///
    /// [commands]: crate::cmd
    fn once()
    where
        Self: Sized;
}

/// A configuration struct for a [`Widget`]
///
/// This configuration is used to make adjustments on how a widget
/// will be added to a file or a window. These adjustments are
/// primarily configurations for the widget itself, and to what
/// direction it will be pushed:
///
/// ```rust
/// # use duat_core::{
/// #     hooks::{self, OnFileOpen},
/// #     ui::Ui,
/// #     widgets::{LineNumbers, Widget},
/// # };
/// # fn test<U: Ui>() {
/// hooks::add::<OnFileOpen<U>>(|builder| {
///     // Change pushing direction to the right.
///     let cfg = LineNumbers::cfg().on_the_right();
///     // Changes to where the numbers will be placed.
///     let cfg = cfg.align_right().align_main_left();
///
///     builder.push(cfg);
/// });
/// # }
/// ```
pub trait WidgetCfg<U>: Sized
where
    U: Ui,
{
    type Widget: Widget<U>;

    fn build(self, on_file: bool) -> (Self::Widget, impl Fn() -> bool + 'static, PushSpecs);
}

// Elements related to the [`Widget`]s
pub struct Node<U: Ui> {
    widget: RwData<dyn Widget<U>>,
    area: U::Area,
    cursors: RwData<Cursors>,

    checker: Arc<dyn Fn() -> bool>,
    busy_updating: Arc<AtomicBool>,

    related_widgets: Option<RwData<Vec<Node<U>>>>,
    on_focus: fn(&Node<U>),
    on_unfocus: fn(&Node<U>),
}

impl<U: Ui> Node<U> {
    pub fn new<W: Widget<U>>(
        widget: RwData<dyn Widget<U>>,
        area: U::Area,
        checker: impl Fn() -> bool + 'static,
    ) -> Self {
        let (cursors, related_widgets) = widget
            .inspect_as(|file: &File| {
                let cursors = crate::cache::load_cache::<Cursors>(file.path());
                let related = RwData::default();
                (cursors.unwrap_or(Cursors::new_excl()), related)
            })
            .unzip();

        Self {
            widget,
            area,
            cursors: RwData::new(cursors.unwrap_or_default()),

            checker: Arc::new(checker),
            busy_updating: Arc::new(AtomicBool::new(false)),

            related_widgets,
            on_focus: Self::on_focus_fn::<W>,
            on_unfocus: Self::on_unfocus_fn::<W>,
        }
    }

    pub fn widget(&self) -> &RwData<dyn Widget<U>> {
        &self.widget
    }

    /// Returns the downcast ref of this [`Widget`].
    pub fn try_downcast<W>(&self) -> Option<RwData<W>> {
        self.widget.try_downcast()
    }

    pub fn data_is<W: 'static>(&self) -> bool {
        self.widget.data_is::<W>()
    }

    pub fn update_and_print(&self) {
        self.busy_updating.store(true, Ordering::Release);

        let mut widget = self.widget.raw_write();
        widget.update(&self.area);
        widget.print(&self.area);

        self.busy_updating.store(false, Ordering::Release);
    }

    pub fn inspect_as<W: 'static, B>(&self, f: impl FnOnce(&W) -> B) -> Option<B> {
        self.widget.inspect_as(f)
    }

    pub fn ptr_eq<W, D>(&self, other: &D) -> bool
    where
        W: ?Sized,
        D: Data<W> + ?Sized,
    {
        self.widget.ptr_eq(other)
    }

    pub fn needs_update(&self) -> bool {
        if !self.busy_updating.load(Ordering::Acquire) {
            (self.checker)() || self.area.has_changed()
        } else {
            false
        }
    }

    pub(crate) fn update(&self) {
        self.widget.raw_write().update(&self.area)
    }

    pub(crate) fn as_active(&self) -> (&RwData<dyn Widget<U>>, &U::Area, &RwData<Cursors>) {
        // Since this function is only ever used on widgets that became active
        // via `command::set_mode`, tecnically speaking, every widget is
        // active, so no need to return an `Option`.
        (&self.widget, &self.area, &self.cursors)
    }

    pub(crate) fn as_file(&self) -> Option<FileParts<U>> {
        self.widget.try_downcast().map(|file| {
            (
                file,
                self.area.clone(),
                self.cursors.clone(),
                self.related_widgets.clone().unwrap(),
            )
        })
    }

    pub(crate) fn on_focus(&self) {
        self.area.set_as_active();
        (self.on_focus)(self)
    }

    pub(crate) fn on_unfocus(&self) {
        (self.on_unfocus)(self)
    }

    pub(crate) fn raw_inspect<B>(&self, f: impl FnOnce(&dyn Widget<U>) -> B) -> B {
        let widget = self.widget.raw_read();
        f(&*widget)
    }

    pub(crate) fn area(&self) -> &U::Area {
        &self.area
    }

    pub(crate) fn related_widgets(&self) -> Option<&RwData<Vec<Node<U>>>> {
        self.related_widgets.as_ref()
    }

    fn on_focus_fn<W: Widget<U>>(&self) {
        self.cursors.inspect(|c| {
            let mut widget = self.widget.write();
            let cfg = widget.print_cfg();
            widget.text_mut().remove_cursors(c, &self.area, cfg);
            widget.on_focus(&self.area);
        });

        self.area.set_as_active();
        let widget = self.widget.try_downcast().unwrap();

        hooks::trigger::<FocusedOn<W, U>>((widget, self.area.clone(), self.cursors.clone()));
    }

    fn on_unfocus_fn<W: Widget<U>>(&self) {
        self.cursors.inspect(|c| {
            let mut widget = self.widget.write();
            let cfg = widget.print_cfg();
            widget.text_mut().remove_cursors(c, &self.area, cfg);
            widget.on_unfocus(&self.area);
        });

        let widget = self.widget.try_downcast().unwrap();

        hooks::trigger::<UnfocusedFrom<W, U>>((widget, self.area.clone(), self.cursors.clone()));
    }
}

impl<U: Ui> Clone for Node<U> {
    fn clone(&self) -> Self {
        Self {
            widget: self.widget.clone(),
            area: self.area.clone(),
            cursors: self.cursors.clone(),
            checker: self.checker.clone(),
            busy_updating: self.busy_updating.clone(),
            related_widgets: self.related_widgets.clone(),
            on_focus: self.on_focus,
            on_unfocus: self.on_unfocus,
        }
    }
}

unsafe impl<U: Ui> Send for Node<U> {}
unsafe impl<U: Ui> Sync for Node<U> {}