-
Notifications
You must be signed in to change notification settings - Fork 662
Expand file tree
/
Copy pathtui.rs
More file actions
4000 lines (3508 loc) · 151 KB
/
tui.rs
File metadata and controls
4000 lines (3508 loc) · 151 KB
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
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! An immediate mode UI framework for terminals.
//!
//! # Why immediate mode?
//!
//! This uses an "immediate mode" design, similar to [ImGui](https://github.com/ocornut/imgui).
//! The reason for this is that I expect the UI needs for any terminal application to be
//! fairly minimal, and for that purpose an immediate mode design is much simpler to use.
//!
//! So what's "immediate mode"? The primary alternative is called "retained mode".
//! The difference is that when you create a button in this framework in one frame,
//! and you stop telling this framework in the next frame, the button will vanish.
//! When you use a regular retained mode UI framework, you create the button once,
//! set up callbacks for when it is clicked, and then stop worrying about it.
//!
//! The downside of immediate mode is that your UI code _may_ become cluttered.
//! The upside however is that you cannot leak UI elements, you don't need to
//! worry about lifetimes nor callbacks, and that simple UIs are simple to write.
//!
//! More importantly though, the primary reason for this is that the
//! lack of callbacks means we can use this design across a plain C ABI,
//! which we'll need once plugins come into play. GTK's `g_signal_connect`
//! shows that the alternative can be rather cumbersome.
//!
//! # Design overview
//!
//! While this file is fairly lengthy, the overall algorithm is simple.
//! On the first frame ever:
//! * Prepare an empty `arena_next`.
//! * Parse the incoming [`input::Input`] which should be a resize event.
//! * Create a new [`Context`] instance and give it the caller.
//! * Now the caller will draw their UI with the [`Context`] by calling the
//! various [`Context`] UI methods, such as [`Context::block_begin()`] and
//! [`Context::block_end()`]. These two are the basis which all other UI
//! elements are built upon by the way. Each UI element that is created gets
//! allocated onto `arena_next` and inserted into the UI tree.
//! That tree works exactly like the DOM tree in HTML: Each node in the tree
//! has a parent, children, and siblings. The tree layout at the end is then
//! a direct mirror of the code "layout" that created it.
//! * Once the caller is done and drops the [`Context`], it'll secretly call
//! `report_context_completion`. This causes a number of things:
//! * The DOM tree that was built is stored in `prev_tree`.
//! * A hashmap of all nodes is built and stored in `prev_node_map`.
//! * `arena_next` is swapped with `arena_prev`.
//! * Each UI node is measured and laid out.
//! * Now the caller is expected to repeat this process with a [`None`]
//! input event until [`Tui::needs_settling()`] returns false.
//! This is necessary, because when [`Context::button()`] returns `true`
//! in one frame, it may change the state in the caller's code
//! and require another frame to be drawn.
//! * Finally a call to [`Tui::render()`] will render the UI tree into the
//! framebuffer and return VT output.
//!
//! On every subsequent frame the process is similar, but one crucial element
//! of any immediate mode UI framework is added:
//! Now when the caller draws their UI, the various [`Context`] UI elements
//! have access to `prev_node_map` and the previously built UI tree.
//! This allows the UI framework to reuse the previously computed layout for
//! hit tests, caching scroll offsets, and so on.
//!
//! In the end it looks very similar:
//! * Prepare an empty `arena_next`.
//! * Parse the incoming [`input::Input`]...
//! * **BUT** now we can hit-test mouse clicks onto the previously built
//! UI tree. This way we can delegate focus on left mouse clicks.
//! * Create a new [`Context`] instance and give it the caller.
//! * The caller draws their UI with the [`Context`]...
//! * **BUT** we can preserve the UI state across frames.
//! * Continue rendering until [`Tui::needs_settling()`] returns false.
//! * And the final call to [`Tui::render()`].
//!
//! # Classnames and node IDs
//!
//! So how do we find which node from the previous tree correlates to the
//! current node? Each node needs to be constructed with a "classname".
//! The classname is hashed with the parent node ID as the seed. This derived
//! hash is then used as the new child node ID. Under the assumption that the
//! collision likelihood of the hash function is low, this serves as true IDs.
//!
//! This has the nice added property that finding a node with the same ID
//! guarantees that all of the parent nodes must have equivalent IDs as well.
//! This turns "is the focus anywhere inside this subtree" into an O(1) check.
//!
//! The reason "classnames" are used is because I was hoping to add theming
//! in the future with a syntax similar to CSS (simplified, however).
//!
//! # Example
//!
//! ```
//! use edit::helpers::Size;
//! use edit::input::Input;
//! use edit::tui::*;
//! use edit::{arena, arena_format};
//!
//! struct State {
//! counter: i32,
//! }
//!
//! fn main() {
//! arena::init(128 * 1024 * 1024).unwrap();
//!
//! // Create a `Tui` instance which holds state across frames.
//! let mut tui = Tui::new().unwrap();
//! let mut state = State { counter: 0 };
//! let input = Input::Resize(Size { width: 80, height: 24 });
//!
//! // Pass the input to the TUI.
//! {
//! let mut ctx = tui.create_context(Some(input));
//! draw(&mut ctx, &mut state);
//! }
//!
//! // Continue until the layout has settled.
//! while tui.needs_settling() {
//! let mut ctx = tui.create_context(None);
//! draw(&mut ctx, &mut state);
//! }
//!
//! // Render the output.
//! let scratch = arena::scratch_arena(None);
//! let output = tui.render(&*scratch);
//! println!("{}", output);
//! }
//!
//! fn draw(ctx: &mut Context, state: &mut State) {
//! ctx.table_begin("classname");
//! {
//! ctx.table_next_row();
//!
//! // Thanks to the lack of callbacks, we can use a primitive
//! // if condition here, as well as in any potential C code.
//! if ctx.button("button", "Click me!", ButtonStyle::default()) {
//! state.counter += 1;
//! }
//!
//! // Similarly, formatting and showing labels is straightforward.
//! // It's impossible to forget updating the label this way.
//! ctx.label("label", &arena_format!(ctx.arena(), "Counter: {}", state.counter));
//! }
//! ctx.table_end();
//! }
//! ```
use std::arch::breakpoint;
#[cfg(debug_assertions)]
use std::collections::HashSet;
use std::fmt::Write as _;
use std::{iter, mem, ptr, time};
use crate::arena::{Arena, ArenaString, scratch_arena};
use crate::buffer::{CursorMovement, RcTextBuffer, TextBuffer, TextBufferCell};
use crate::cell::*;
use crate::document::WriteableDocument;
use crate::framebuffer::{Attributes, Framebuffer, INDEXED_COLORS_COUNT, IndexedColor};
use crate::hash::*;
use crate::helpers::*;
use crate::input::{InputKeyMod, kbmod, vk};
use crate::{apperr, arena_format, input, unicode};
const ROOT_ID: u64 = 0x14057B7EF767814F; // Knuth's MMIX constant
const SHIFT_TAB: InputKey = vk::TAB.with_modifiers(kbmod::SHIFT);
const KBMOD_FOR_WORD_NAV: InputKeyMod =
if cfg!(target_os = "macos") { kbmod::ALT } else { kbmod::CTRL };
type Input<'input> = input::Input<'input>;
type InputKey = input::InputKey;
type InputMouseState = input::InputMouseState;
type InputText<'input> = input::InputText<'input>;
/// Since [`TextBuffer`] creation and management is expensive,
/// we cache instances of them for reuse between frames.
/// This is used for [`Context::editline()`].
struct CachedTextBuffer {
node_id: u64,
editor: RcTextBuffer,
seen: bool,
}
/// Since [`Context::editline()`] and [`Context::textarea()`]
/// do almost the same thing, this abstracts over the two.
enum TextBufferPayload<'a> {
Editline(&'a mut dyn WriteableDocument),
Textarea(RcTextBuffer),
}
/// In order for the TUI to show the correct Ctrl/Alt/Shift
/// translations, this struct lets you set them.
pub struct ModifierTranslations {
pub ctrl: &'static str,
pub alt: &'static str,
pub shift: &'static str,
}
/// Controls to which node the floater is anchored.
#[derive(Default, Clone, Copy, PartialEq, Eq)]
pub enum Anchor {
/// The floater is attached relative to the node created last.
#[default]
Last,
/// The floater is attached relative to the current node (= parent of new nodes).
Parent,
/// The floater is attached relative to the root node (= usually the viewport).
Root,
}
/// Controls the position of the floater. See [`Context::attr_float`].
#[derive(Default)]
pub struct FloatSpec {
/// Controls to which node the floater is anchored.
pub anchor: Anchor,
// Specifies the origin of the container relative to the container size. [0, 1]
pub gravity_x: f32,
pub gravity_y: f32,
// Specifies an offset from the origin in cells.
pub offset_x: f32,
pub offset_y: f32,
}
/// Informs you about the change that was made to the list selection.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ListSelection {
/// The selection wasn't changed.
Unchanged,
/// The selection was changed to the current list item.
Selected,
/// The selection was changed to the current list item
/// *and* the item was also activated (Enter or Double-click).
Activated,
}
/// Controls the position of a node relative to its parent.
#[derive(Default)]
pub enum Position {
/// The child is stretched to fill the parent.
#[default]
Stretch,
/// The child is positioned at the left edge of the parent.
Left,
/// The child is positioned at the center of the parent.
Center,
/// The child is positioned at the right edge of the parent.
Right,
}
/// Controls the text overflow behavior of a label
/// when the text doesn't fit the container.
#[derive(Default, Clone, Copy, PartialEq, Eq)]
pub enum Overflow {
/// Text is simply cut off when it doesn't fit.
#[default]
Clip,
/// An ellipsis is shown at the end of the text.
TruncateHead,
/// An ellipsis is shown in the middle of the text.
TruncateMiddle,
/// An ellipsis is shown at the beginning of the text.
TruncateTail,
}
/// If specified, a checkmark 🗹 / ☐ to be prepended to the button text
#[derive(Default, Clone, Copy, PartialEq, Eq)]
enum Checkmark {
/// No checkmark (or space for it)
#[default]
Absent,
/// Add whitespace where a checkmark would be
/// (to keep aligned with checkmark'd buttons)
AlignOnly,
/// Add a 🗹 prefix
Checked,
/// Add a ☐ prefix
Unchecked,
}
impl From<bool> for Checkmark {
fn from(checked: bool) -> Self {
if checked { Checkmark::Checked } else { Checkmark::Unchecked }
}
}
/// Controls the style with which a button label renders
#[derive(Clone, Copy)]
pub struct ButtonStyle {
accelerator: Option<char>,
checkmark: Checkmark,
bracketed: bool,
}
impl ButtonStyle {
/// Draw an accelerator label: `[_E_xample button]` or `[Example button(X)]`
///
/// Must provide an upper-case ASCII character.
pub fn accelerator(self, char: char) -> Self {
Self { accelerator: Some(char), ..self }
}
/// Draw with or without brackets: `[Example Button]` or `Example Button`
pub fn bracketed(self, bracketed: bool) -> Self {
Self { bracketed, ..self }
}
/// Draw a checkbox prefix: `[🗹 Example Button]`
///
/// Note: use `checkbox` or `menubar_menu_checkbox`
fn checkmark(self, checkmark: Checkmark) -> Self {
Self { checkmark, ..self }
}
}
impl Default for ButtonStyle {
fn default() -> Self {
Self {
accelerator: None,
checkmark: Checkmark::Absent,
bracketed: true, // Default style for most buttons. Brackets may be disabled e.g. for buttons in menus
}
}
}
/// There's two types of lifetimes the TUI code needs to manage:
/// * Across frames
/// * Per frame
///
/// [`Tui`] manages the first one. It's also the entrypoint for
/// everything else you may want to do.
pub struct Tui {
/// Arena used for the previous frame.
arena_prev: Arena,
/// Arena used for the current frame.
arena_next: Arena,
/// The UI tree built in the previous frame.
/// This refers to memory in `arena_prev`.
prev_tree: Tree<'static>,
/// A hashmap of all nodes built in the previous frame.
/// This refers to memory in `arena_prev`.
prev_node_map: NodeMap<'static>,
/// The framebuffer used for rendering.
framebuffer: Framebuffer,
modifier_translations: ModifierTranslations,
floater_default_bg: u32,
floater_default_fg: u32,
modal_default_bg: u32,
modal_default_fg: u32,
/// Last known terminal size.
///
/// This lives here instead of [`Context`], because we need to
/// track the state across frames and input events.
/// This also applies to the remaining members in this block below.
size: Size,
/// Last known mouse position.
mouse_position: Point,
/// Between mouse down and up, the position where the mouse was pressed.
/// Otherwise, this contains Point::MIN.
mouse_down_position: Point,
/// Node ID of the node that was clicked on.
/// Used for tracking drag targets.
left_mouse_down_target: u64,
/// Timestamp of the last mouse up event.
/// Used for tracking double/triple clicks.
mouse_up_timestamp: std::time::Instant,
/// The current mouse state.
mouse_state: InputMouseState,
/// Whether the mouse is currently being dragged.
mouse_is_drag: bool,
/// The number of clicks that have happened in a row.
/// Gets reset when the mouse was released for a while.
mouse_click_counter: CoordType,
/// The path to the node that was clicked on.
mouse_down_node_path: Vec<u64>,
/// The position of the first click in a double/triple click series.
first_click_position: Point,
/// The node ID of the node that was first clicked on
/// in a double/triple click series.
first_click_target: u64,
/// Path to the currently focused node.
focused_node_path: Vec<u64>,
/// Contains the last element in [`Tui::focused_node_path`].
/// This way we can track if the focus changed, because then we
/// need to scroll the node into view if it's within a scrollarea.
focused_node_for_scrolling: u64,
/// A list of cached text buffers used for [`Context::editline()`].
cached_text_buffers: Vec<CachedTextBuffer>,
/// The clipboard contents.
clipboard: Vec<u8>,
/// A counter that is incremented every time the clipboard changes.
/// Allows for tracking clipboard changes without comparing contents.
clipboard_generation: u32,
settling_have: i32,
settling_want: i32,
read_timeout: time::Duration,
}
impl Tui {
/// Creates a new [`Tui`] instance for storing state across frames.
pub fn new() -> apperr::Result<Self> {
let arena_prev = Arena::new(128 * MEBI)?;
let arena_next = Arena::new(128 * MEBI)?;
// SAFETY: Since `prev_tree` refers to `arena_prev`/`arena_next`, from its POV the lifetime
// is `'static`, requiring us to use `transmute` to circumvent the borrow checker.
let prev_tree = Tree::new(unsafe { mem::transmute::<&Arena, &Arena>(&arena_next) });
let mut tui = Self {
arena_prev,
arena_next,
prev_tree,
prev_node_map: Default::default(),
framebuffer: Framebuffer::new(),
modifier_translations: ModifierTranslations {
ctrl: "Ctrl",
alt: "Alt",
shift: "Shift",
},
floater_default_bg: 0,
floater_default_fg: 0,
modal_default_bg: 0,
modal_default_fg: 0,
size: Size { width: 0, height: 0 },
mouse_position: Point::MIN,
mouse_down_position: Point::MIN,
left_mouse_down_target: 0,
mouse_up_timestamp: std::time::Instant::now(),
mouse_state: InputMouseState::None,
mouse_is_drag: false,
mouse_click_counter: 0,
mouse_down_node_path: Vec::with_capacity(16),
first_click_position: Point::MIN,
first_click_target: 0,
focused_node_path: Vec::with_capacity(16),
focused_node_for_scrolling: ROOT_ID,
cached_text_buffers: Vec::with_capacity(16),
clipboard: Vec::new(),
clipboard_generation: 0,
settling_have: 0,
settling_want: 0,
read_timeout: time::Duration::MAX,
};
Self::clean_node_path(&mut tui.mouse_down_node_path);
Self::clean_node_path(&mut tui.focused_node_path);
Ok(tui)
}
/// Sets up the framebuffer's color palette.
pub fn setup_indexed_colors(&mut self, colors: [u32; INDEXED_COLORS_COUNT]) {
self.framebuffer.set_indexed_colors(colors);
}
/// Set up translations for Ctrl/Alt/Shift modifiers.
pub fn setup_modifier_translations(&mut self, translations: ModifierTranslations) {
self.modifier_translations = translations;
}
/// Set the default background color for floaters (dropdowns, etc.).
pub fn set_floater_default_bg(&mut self, color: u32) {
self.floater_default_bg = color;
}
/// Set the default foreground color for floaters (dropdowns, etc.).
pub fn set_floater_default_fg(&mut self, color: u32) {
self.floater_default_fg = color;
}
/// Set the default background color for modals.
pub fn set_modal_default_bg(&mut self, color: u32) {
self.modal_default_bg = color;
}
/// Set the default foreground color for modals.
pub fn set_modal_default_fg(&mut self, color: u32) {
self.modal_default_fg = color;
}
/// If the TUI is currently running animations, etc.,
/// this will return a timeout smaller than [`time::Duration::MAX`].
pub fn read_timeout(&mut self) -> time::Duration {
mem::replace(&mut self.read_timeout, time::Duration::MAX)
}
/// Returns the viewport size.
pub fn size(&self) -> Size {
// We don't use the size stored in the framebuffer, because until
// `render()` is called, the framebuffer will use a stale size.
self.size
}
/// Returns an indexed color from the framebuffer.
#[inline]
pub fn indexed(&self, index: IndexedColor) -> u32 {
self.framebuffer.indexed(index)
}
/// Returns an indexed color from the framebuffer with the given alpha.
/// See [`Framebuffer::indexed_alpha()`].
#[inline]
pub fn indexed_alpha(&self, index: IndexedColor, numerator: u32, denominator: u32) -> u32 {
self.framebuffer.indexed_alpha(index, numerator, denominator)
}
/// Returns a color in contrast with the given color.
/// See [`Framebuffer::contrasted()`].
pub fn contrasted(&self, color: u32) -> u32 {
self.framebuffer.contrasted(color)
}
/// Returns the current clipboard contents.
pub fn clipboard(&self) -> &[u8] {
&self.clipboard
}
/// Returns the current clipboard generation.
/// The generation changes every time the clipboard contents change.
/// This allows you to track clipboard changes.
pub fn clipboard_generation(&self) -> u32 {
self.clipboard_generation
}
/// Starts a new frame and returns a [`Context`] for it.
pub fn create_context<'a, 'input>(
&'a mut self,
input: Option<Input<'input>>,
) -> Context<'a, 'input> {
// SAFETY: Since we have a unique `&mut self`, nothing is holding onto `arena_prev`,
// which will become `arena_next` and get reset. It's safe to reset and reuse its memory.
mem::swap(&mut self.arena_prev, &mut self.arena_next);
unsafe { self.arena_next.reset(0) };
// In the input handler below we transformed a mouse up into a release event.
// Now, a frame later, we must reset it back to none, to stop it from triggering things.
// Same for Scroll events.
if self.mouse_state > InputMouseState::Right {
self.mouse_down_position = Point::MIN;
self.mouse_down_node_path.clear();
self.left_mouse_down_target = 0;
self.mouse_state = InputMouseState::None;
self.mouse_is_drag = false;
}
let now = std::time::Instant::now();
let mut input_text = None;
let mut input_keyboard = None;
let mut input_mouse_modifiers = kbmod::NONE;
let mut input_mouse_click = 0;
let mut input_scroll_delta = Point { x: 0, y: 0 };
// `input_consumed` should be `true` if we're in the settling phase which is indicated by
// `self.needs_settling() == true`. However, there's a possibility for it being true from
// a previous frame, and we do have fresh new input. In that case want `input_consumed`
// to be false of course which is ensured by checking for `input.is_none()`.
let input_consumed = self.needs_settling() && input.is_none();
if self.scroll_to_focused() {
self.needs_more_settling();
}
match input {
None => {}
Some(Input::Resize(resize)) => {
assert!(resize.width > 0 && resize.height > 0);
assert!(resize.width < 32768 && resize.height < 32768);
self.size = resize;
}
Some(Input::Text(text)) => {
input_text = Some(text);
// TODO: the .len()==1 check causes us to ignore keyboard inputs that are faster than we process them.
// For instance, imagine the user presses "A" twice and we happen to read it in a single chunk.
// This causes us to ignore the keyboard input here. We need a way to inform the caller over
// how much of the input text we actually processed in a single frame. Or perhaps we could use
// the needs_settling logic?
if !text.bracketed && text.text.len() == 1 {
let ch = text.text.as_bytes()[0];
input_keyboard = InputKey::from_ascii(ch as char)
}
}
Some(Input::Keyboard(keyboard)) => {
input_keyboard = Some(keyboard);
}
Some(Input::Mouse(mouse)) => {
let mut next_state = mouse.state;
let next_position = mouse.position;
let next_scroll = mouse.scroll;
let mouse_down = self.mouse_state == InputMouseState::None
&& next_state != InputMouseState::None;
let mouse_up = self.mouse_state != InputMouseState::None
&& next_state == InputMouseState::None;
let is_drag = self.mouse_state == InputMouseState::Left
&& next_state == InputMouseState::Left
&& next_position != self.mouse_position;
let mut hovered_node = None; // Needed for `mouse_down`
let mut focused_node = None; // Needed for `mouse_down` and `is_click`
if mouse_down || mouse_up {
for root in self.prev_tree.iterate_roots() {
Tree::visit_all(root, root, true, |node| {
let n = node.borrow();
if !n.outer_clipped.contains(next_position) {
// Skip the entire sub-tree, because it doesn't contain the cursor.
return VisitControl::SkipChildren;
}
hovered_node = Some(node);
if n.attributes.focusable {
focused_node = Some(node);
}
VisitControl::Continue
});
}
}
if mouse_down {
// Transition from no mouse input to some mouse input --> Record the mouse down position.
Self::build_node_path(hovered_node, &mut self.mouse_down_node_path);
// On left-mouse-down we change focus.
let mut target = 0;
if next_state == InputMouseState::Left {
target = focused_node.map_or(0, |n| n.borrow().id);
Self::build_node_path(focused_node, &mut self.focused_node_path);
self.needs_more_settling(); // See `needs_more_settling()`.
}
// Double-/Triple-/Etc.-clicks are triggered on mouse-down,
// unlike the first initial click, which is triggered on mouse-up.
if self.mouse_click_counter != 0 {
if self.first_click_target != target
|| self.first_click_position != next_position
|| (now - self.mouse_up_timestamp)
> std::time::Duration::from_millis(500)
{
// If the cursor moved / the focus changed in between, or if the user did a slow click,
// we reset the click counter. On mouse-up it'll transition to a regular click.
self.mouse_click_counter = 0;
self.first_click_position = Point::MIN;
self.first_click_target = 0;
} else {
self.mouse_click_counter += 1;
input_mouse_click = self.mouse_click_counter;
};
}
// Gets reset at the start of this function.
self.left_mouse_down_target = target;
self.mouse_down_position = next_position;
} else if mouse_up {
// Transition from some mouse input to no mouse input --> The mouse button was released.
next_state = InputMouseState::Release;
let target = focused_node.map_or(0, |n| n.borrow().id);
if self.left_mouse_down_target == 0 || self.left_mouse_down_target != target {
// If `left_mouse_down_target == 0`, then it wasn't a left-click, in which case
// the target gets reset. Same, if the focus changed in between any clicks.
self.mouse_click_counter = 0;
self.first_click_position = Point::MIN;
self.first_click_target = 0;
} else if self.mouse_click_counter == 0 {
// No focus change, and no previous clicks? This is an initial, regular click.
self.mouse_click_counter = 1;
self.first_click_position = self.mouse_down_position;
self.first_click_target = target;
input_mouse_click = 1;
}
self.mouse_up_timestamp = now;
} else if is_drag {
self.mouse_is_drag = true;
}
input_mouse_modifiers = mouse.modifiers;
input_scroll_delta = next_scroll;
self.mouse_position = next_position;
self.mouse_state = next_state;
}
}
if !input_consumed {
// Every time there's input, we naturally need to re-render at least once.
self.settling_have = 0;
self.settling_want = 1;
}
// TODO: There should be a way to do this without unsafe.
// Allocating from the arena borrows the arena, and so allocating the tree here borrows self.
// This conflicts with us passing a mutable reference to `self` into the struct below.
let tree = Tree::new(unsafe { mem::transmute::<&Arena, &Arena>(&self.arena_next) });
Context {
tui: self,
input_text,
input_keyboard,
input_mouse_modifiers,
input_mouse_click,
input_scroll_delta,
input_consumed,
tree,
last_modal: None,
focused_node: None,
next_block_id_mixin: 0,
needs_settling: false,
#[cfg(debug_assertions)]
seen_ids: HashSet::new(),
}
}
fn report_context_completion<'a>(&'a mut self, ctx: &mut Context<'a, '_>) {
// If this hits, you forgot to block_end() somewhere. The best way to figure
// out where is to do a binary search of commenting out code in main.rs.
debug_assert!(
ctx.tree.current_node.borrow().stack_parent.is_none(),
"Dangling parent! Did you miss a block_end?"
);
// End the root node.
ctx.block_end();
// Ensure that focus doesn't escape the active modal.
if let Some(node) = ctx.last_modal
&& !self.is_subtree_focused(&node.borrow())
{
ctx.steal_focus_for(node);
}
// If nodes have appeared or disappeared, we need to re-render.
// Same, if the focus has changed (= changes the highlight color, etc.).
let mut needs_settling = ctx.needs_settling;
needs_settling |= self.prev_tree.checksum != ctx.tree.checksum;
// Adopt the new tree and recalculate the node hashmap.
//
// SAFETY: The memory used by the tree is owned by the `self.arena_next` right now.
// Stealing the tree here thus doesn't need to copy any memory unless someone resets the arena.
// (The arena is reset in `reset()` above.)
unsafe {
self.prev_tree = mem::transmute_copy(&ctx.tree);
self.prev_node_map = NodeMap::new(mem::transmute(&self.arena_next), &self.prev_tree);
}
let mut focus_path_pop_min = 0;
// If the user pressed Escape, we move the focus to a parent node.
if !ctx.input_consumed && ctx.consume_shortcut(vk::ESCAPE) {
focus_path_pop_min = 1;
}
// Remove any unknown nodes from the focus path.
// It's important that we do this after the tree has been swapped out,
// so that pop_focusable_node() has access to the newest version of the tree.
needs_settling |= self.pop_focusable_node(focus_path_pop_min);
// `needs_more_settling()` depends on the current value
// of `settling_have` and so we increment it first.
self.settling_have += 1;
if needs_settling {
self.needs_more_settling();
}
// Remove cached text editors that are no longer in use.
self.cached_text_buffers.retain(|c| c.seen);
for root in Tree::iterate_siblings(Some(self.prev_tree.root_first)) {
let mut root = root.borrow_mut();
root.compute_intrinsic_size();
}
let viewport = self.size.as_rect();
for root in Tree::iterate_siblings(Some(self.prev_tree.root_first)) {
let mut root = root.borrow_mut();
let root = &mut *root;
if let Some(float) = &root.attributes.float {
let mut x = 0;
let mut y = 0;
if let Some(node) = root.parent {
let node = node.borrow();
x = node.outer.left;
y = node.outer.top;
}
let size = root.intrinsic_to_outer();
x += (float.offset_x - float.gravity_x * size.width as f32) as CoordType;
y += (float.offset_y - float.gravity_y * size.height as f32) as CoordType;
root.outer.left = x;
root.outer.top = y;
root.outer.right = x + size.width;
root.outer.bottom = y + size.height;
root.outer = root.outer.intersect(viewport);
} else {
root.outer = viewport;
}
root.inner = root.outer_to_inner(root.outer);
root.outer_clipped = root.outer;
root.inner_clipped = root.inner;
let outer = root.outer;
root.layout_children(outer);
}
}
fn build_node_path(node: Option<&NodeCell>, path: &mut Vec<u64>) {
path.clear();
if let Some(mut node) = node {
loop {
let n = node.borrow();
path.push(n.id);
node = match n.parent {
Some(parent) => parent,
None => break,
};
}
path.reverse();
} else {
path.push(ROOT_ID);
}
}
fn clean_node_path(path: &mut Vec<u64>) {
Self::build_node_path(None, path);
}
/// After you finished processing all input, continue redrawing your UI until this returns false.
pub fn needs_settling(&mut self) -> bool {
self.settling_have <= self.settling_want
}
fn needs_more_settling(&mut self) {
// If the focus has changed, the new node may need to be re-rendered.
// Same, every time we encounter a previously unknown node via `get_prev_node`,
// because that means it likely failed to get crucial information such as the layout size.
if cfg!(debug_assertions) && self.settling_have == 15 {
breakpoint();
}
self.settling_want = (self.settling_have + 1).min(20);
}
/// Renders the last frame into the framebuffer and returns the VT output.
pub fn render<'a>(&mut self, arena: &'a Arena) -> ArenaString<'a> {
self.framebuffer.flip(self.size);
for child in self.prev_tree.iterate_roots() {
let mut child = child.borrow_mut();
self.render_node(&mut child);
}
self.framebuffer.render(arena)
}
/// Recursively renders each node and its children.
#[allow(clippy::only_used_in_recursion)]
fn render_node(&mut self, node: &mut Node) {
let outer_clipped = node.outer_clipped;
if outer_clipped.is_empty() {
return;
}
let scratch = scratch_arena(None);
if node.attributes.bordered {
// ┌────┐
{
let mut fill = ArenaString::new_in(&scratch);
fill.push('┌');
fill.push_repeat('─', (outer_clipped.right - outer_clipped.left - 2) as usize);
fill.push('┐');
self.framebuffer.replace_text(
outer_clipped.top,
outer_clipped.left,
outer_clipped.right,
&fill,
);
}
// │ │
{
let mut fill = ArenaString::new_in(&scratch);
fill.push('│');
fill.push_repeat(' ', (outer_clipped.right - outer_clipped.left - 2) as usize);
fill.push('│');
for y in outer_clipped.top + 1..outer_clipped.bottom - 1 {
self.framebuffer.replace_text(
y,
outer_clipped.left,
outer_clipped.right,
&fill,
);
}
}
// └────┘
{
let mut fill = ArenaString::new_in(&scratch);
fill.push('└');
fill.push_repeat('─', (outer_clipped.right - outer_clipped.left - 2) as usize);
fill.push('┘');
self.framebuffer.replace_text(
outer_clipped.bottom - 1,
outer_clipped.left,
outer_clipped.right,
&fill,
);
}
}
if node.attributes.float.is_some() && node.attributes.bg & 0xff000000 == 0xff000000 {
if !node.attributes.bordered {
let mut fill = ArenaString::new_in(&scratch);
fill.push_repeat(' ', (outer_clipped.right - outer_clipped.left) as usize);
for y in outer_clipped.top..outer_clipped.bottom {
self.framebuffer.replace_text(
y,
outer_clipped.left,
outer_clipped.right,
&fill,
);
}
}
self.framebuffer.replace_attr(outer_clipped, Attributes::All, Attributes::None);
}
self.framebuffer.blend_bg(outer_clipped, node.attributes.bg);
self.framebuffer.blend_fg(outer_clipped, node.attributes.fg);
if node.attributes.reverse {
self.framebuffer.reverse(outer_clipped);
}
let inner = node.inner;
let inner_clipped = node.inner_clipped;
if inner_clipped.is_empty() {
return;
}
match &mut node.content {
NodeContent::Modal(title) => {
if !title.is_empty() {
self.framebuffer.replace_text(
node.outer.top,
node.outer.left + 2,
node.outer.right - 1,
title,
);
}
}
NodeContent::Text(content) => self.render_styled_text(
inner,
node.intrinsic_size.width,
&content.text,
&content.chunks,
content.overflow,
),
NodeContent::Textarea(tc) => {
let mut tb = tc.buffer.borrow_mut();
let mut destination = Rect {
left: inner_clipped.left,
top: inner_clipped.top,
right: inner_clipped.right,
bottom: inner_clipped.bottom,
};
if !tc.single_line {
// Account for the scrollbar.
destination.right -= 1;
}
if let Some(res) =
tb.render(tc.scroll_offset, destination, tc.has_focus, &mut self.framebuffer)
{
tc.scroll_offset_x_max = res.visual_pos_x_max;
}
if !tc.single_line {
// Render the scrollbar.
let track = Rect {
left: inner_clipped.right - 1,
top: inner_clipped.top,
right: inner_clipped.right,
bottom: inner_clipped.bottom,
};
tc.thumb_height = self.framebuffer.draw_scrollbar(
inner_clipped,
track,
tc.scroll_offset.y,
tb.visual_line_count() + inner.height() - 1,
);