Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_tui/src/lib_tui/window.rs

20.6 KiB, 42 runs

created by r1870400018:1217, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1use crate::lib_tui::{
2 action::Action,
3 cfg::style::StyleLibrary,
4 draw::{
5 outline::{
6 Outline,
7 OutlineConfig,
8 },
9 tab::TabbedTextBox,
10 tbox::TextBox,
11 window::{
12 TextBoxesState,
13 Window,
14 WindowConfig,
15 WindowId,
16 WindowStateInit,
17 },
18 },
19 event::KeyState,
20 text::{
21 nav::PositionCursor,
22 highlight::HighlightBuilder,
23 typ::{
24 HighlightType,
25 TextType,
26 },
27 },
28 render::{
29 Drawer,
30 Renderer,
31 Sink,
32 When,
33 },
34};
35
36use oxedyne_fe2o3_core::{
37 prelude::*,
38 count::{
39 Counter,
40 CycleCounter,
41 },
42};
43use oxedyne_fe2o3_geom::{
44 rect::{
45 AbsSize,
46 RectView,
47 },
48};
49use oxedyne_fe2o3_text::{
50 Text,
51 lines::{
52 LineRange,
53 TextLines,
54 },
55 highlight::{
56 Highlight,
57 Highlighter,
58 },
59};
60
61use std::{
62 collections::{
63 BTreeMap,
64 HashMap,
65 },
66 rc::Rc,
67 sync::{
68 Arc,
69 RwLock,
70 },
71};
72
73
74#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
75pub enum WindowMode {
76 #[default]
77 Navigation,
78 Interaction,
79 WindowManagement,
80 BorderManagement,
81}
82
83#[derive(Clone, Debug, Default)]
84pub struct MenuItem {
85 pub text: Text<TextType>,
86 pub action: Option<Action>,
87}
88
89new_type!(MenuList, Vec<MenuItem>, Clone, Debug, Default);
90
91impl From<Vec<(Text<TextType>, Option<Action>)>> for MenuList {
92 fn from(v: Vec<(Text<TextType>, Option<Action>)>) -> Self {
93 let mut result = Vec::new();
94 for (text, action) in v {
95 result.push(MenuItem {
96 text,
97 action,
98 });
99 }
100 Self(result)
101 }
102}
103
104impl MenuList {
105 pub fn text_lines(self) -> Outcome<TextLines<TextType, HighlightType>> {
106 let lines = self.iter().map(|item| item.text.clone());
107 let actions = self.iter().map(|item| match item.action {
108 Some(action) => Some(HighlightType::Menu(action)),
109 None => None,
110 });
111 let pairs = lines.clone().zip(actions).collect();
112 // Zip the highlight associated data into the highlights.
113 let highlights = res!(HighlightBuilder::IgnoreWhiteSpace.build(pairs));
114 let highlighter = Highlighter::new(highlights, None);
115 Ok(TextLines::new(lines.collect(), Some(highlighter)))
116 }
117}
118
119impl WindowMode {
120
121 pub fn build_menus() -> Outcome<HashMap<Self, TextLines<TextType, HighlightType>>> {
122 let mut map = HashMap::new();
123 let menu_list = MenuList::from(vec![
124 (
125 TextType::new_menu_item("Move to next window (Tab) "),
126 Some(Action::MoveToNextWindow),
127 ),
128 (
129 TextType::new_menu_item("Enter window (Enter) "),
130 Some(Action::EnterWindow),
131 ),
132 (
133 TextType::new_menu_item("Enter window management mode (Space) "),
134 Some(Action::EnterWindowManagementMode),
135 ),
136 (
137 TextType::new_menu_item("Create new window "),
138 Some(Action::CreateNewWindow),
139 ),
140 ]);
141 let text_lines = res!(menu_list.text_lines());
142 map.insert(
143 Self::Navigation,
144 text_lines,
145 );
146 let menu_list = MenuList::from(vec![
147 (
148 TextType::new_menu_item( "Move to next tabbed text box (Tab) "),
149 Some(Action::MoveToNextTab),
150 ),
151 (
152 TextType::new_menu_heading( "Move cursor "),
153 None,
154 ),
155 (
156 TextType::new_menu_item( " Move cursor up (Up arrow) "),
157 Some(Action::MoveCursorUp),
158 ),
159 (
160 TextType::new_menu_item( " Move cursor down (Down arrow) "),
161 Some(Action::MoveCursorDown),
162 ),
163 (
164 TextType::new_menu_item( " Move cursor right (Right arrow) "),
165 Some(Action::MoveCursorRight),
166 ),
167 (
168 TextType::new_menu_item( " Move cursor left (Left arrow) "),
169 Some(Action::MoveCursorLeft),
170 ),
171 (
172 TextType::new_menu_heading( "Pan view "),
173 None,
174 ),
175 (
176 TextType::new_menu_item( " Pan view up (Shift + Up arrow) "),
177 Some(Action::PanTextViewUp),
178 ),
179 (
180 TextType::new_menu_item( " Pan view down (Shift + Down arrow) "),
181 Some(Action::PanTextViewDown),
182 ),
183 (
184 TextType::new_menu_item( " Pan view right (Shift + Right arrow) "),
185 Some(Action::PanTextViewRight),
186 ),
187 (
188 TextType::new_menu_item( " Pan view left (Shift + Left arrow) "),
189 Some(Action::PanTextViewLeft),
190 ),
191 (
192 TextType::new_menu_heading( "Editor modes "),
193 None,
194 ),
195 (
196 TextType::new_menu_item( " Enter editor insert mode (i) "),
197 Some(Action::EnterEditorInsertMode),
198 ),
199 (
200 TextType::new_menu_item( " Enter editor replace mode (r) "),
201 Some(Action::EnterEditorReplaceMode),
202 ),
203 (
204 TextType::new_menu_item( " Enter editor modify mode (m) "),
205 Some(Action::EnterEditorModifyMode),
206 ),
207 (
208 TextType::new_menu_item( "Return to editor navigation mode (Esc) "),
209 Some(Action::ReturnToEditorNavigationMode),
210 ),
211 (
212 TextType::new_menu_item( "Return to window navigation mode (Esc) "),
213 Some(Action::ReturnToWindowNavigationMode),
214 ),
215 ]);
216 let text_lines = res!(menu_list.text_lines());
217 map.insert(
218 Self::Interaction,
219 text_lines,
220 );
221 let menu_list = MenuList::from(vec![
222 (
223 TextType::new_menu_item( "Enter border management mode (Space) "),
224 Some(Action::EnterBorderManagementMode),
225 ),
226 (
227 TextType::new_menu_heading( "Slide window "),
228 None,
229 ),
230 (
231 TextType::new_menu_item( " Slide window up (Up arrow) "),
232 Some(Action::SlideWindowUp),
233 ),
234 (
235 TextType::new_menu_item( " Slide window down (Down arrow) "),
236 Some(Action::SlideWindowDown),
237 ),
238 (
239 TextType::new_menu_item( " Slide window right (Right arrow) "),
240 Some(Action::SlideWindowRight),
241 ),
242 (
243 TextType::new_menu_item( " Slide window left (Left arrow) "),
244 Some(Action::SlideWindowLeft),
245 ),
246 (
247 TextType::new_menu_item( "Create new window (+) "),
248 Some(Action::CreateWindow),
249 ),
250 (
251 TextType::new_menu_item( "Delete window (Del) "),
252 Some(Action::DeleteWindow),
253 ),
254 (
255 TextType::new_menu_item( "Return to navigation mode (Esc) "),
256 Some(Action::ReturnToWindowNavigationMode),
257 ),
258 ]);
259 let text_lines = res!(menu_list.text_lines());
260 map.insert(
261 Self::WindowManagement,
262 text_lines,
263 );
264 let menu_list = MenuList::from(vec![
265 (
266 TextType::new_menu_item( "Move to next border (Space) "),
267 Some(Action::MoveToNextBorder),
268 ),
269 (
270 TextType::new_menu_heading( "Drag border "),
271 None,
272 ),
273 (
274 TextType::new_menu_item( " Drag border up (Up arrow) "),
275 Some(Action::DragBorderUp),
276 ),
277 (
278 TextType::new_menu_item( " Drag border down (Down arrow) "),
279 Some(Action::DragBorderDown),
280 ),
281 (
282 TextType::new_menu_item( " Drag border right (Right arrow) "),
283 Some(Action::DragBorderRight),
284 ),
285 (
286 TextType::new_menu_item( " Drag border left (Left arrow) "),
287 Some(Action::DragBorderLeft),
288 ),
289 (
290 TextType::new_menu_item( "Return to window management mode (Esc) "),
291 Some(Action::ReturnToWindowManagementMode),
292 ),
293 ]);
294 let text_lines = res!(menu_list.text_lines());
295 map.insert(
296 Self::BorderManagement,
297 text_lines,
298 );
299 Ok(map)
300 }
301}
302
303#[derive(Clone, Debug, Default)]
304pub struct WindowManagerConfig {
305 pub tab_string: String,
306 pub window_count_limit: u8,
307 pub window_min_size_x: u8,
308 pub window_min_size_y: u8,
309 pub selection_envelope: OutlineConfig,
310}
311
312impl WindowManagerConfig {
313 pub fn window_min_size(&self) -> AbsSize {
314 AbsSize::from((
315 self.window_min_size_x,
316 self.window_min_size_y,
317 ))
318 }
319}
320
321#[derive(Clone, Debug, Default)]
322pub struct WindowManagerState<'a> {
323 pub selection_envelope: Option<Outline<'a>>,
324 pub window_list: Rc<RwLock<TextLines<TextType, HighlightType>>>,
325 pub menu_text: Arc<RwLock<TextLines<TextType, HighlightType>>>,
326 //pub menu_actions: Vec<Action>,
327 pub next_label: Counter<u8>,
328 pub new_win_pos: (CycleCounter<usize>, CycleCounter<usize>),
329}
330
331#[derive(Clone, Debug, Default)]
332pub struct WindowManager<'a> {
333 pub cfg: WindowManagerConfig,
334 pub windows: BTreeMap<WindowId, Window>,
335 pub mode: WindowMode,
336 pub menus: HashMap<WindowMode, TextLines<TextType, HighlightType>>,
337 pub focus: WindowId,
338 pub state: WindowManagerState<'a>,
339 pub key_state: KeyState,
340 pub style_lib: StyleLibrary,
341}
342
343impl<'a> WindowManager<'a> {
344
345 pub fn new(
346 cfg: WindowManagerConfig,
347 )
348 -> Outcome<Self>
349 {
350 let next_label_counter = res!(Counter::new(3, cfg.window_count_limit, 1));
351 let mut new = Self {
352 cfg,
353 menus: res!(WindowMode::build_menus()),
354 ..Default::default()
355 };
356 new.state.next_label = next_label_counter;
357 new.state.new_win_pos = (
358 res!(CycleCounter::new(30, 100, 2)), // x
359 res!(CycleCounter::new(2, 20, 2)), // y
360 );
361 res!(new.set_mode_update_menu(WindowMode::Navigation, None));
362 Ok(new)
363 }
364
365 pub fn next_id(&mut self) -> Outcome<WindowId> {
366 if let Some(next_label) = self.state.next_label.next() {
367 Ok(WindowId::User(next_label))
368 } else {
369 Err(err!(
370 "Window count limit of {} has been reached.", self.cfg.window_count_limit;
371 Excessive))
372 }
373 }
374
375 pub fn add_window(
376 &mut self,
377 id: Option<WindowId>,
378 cfg: WindowConfig,
379 view: RectView,
380 )
381 -> Outcome<(WindowId, bool)>
382 {
383 let id = match id {
384 Some(id) => id,
385 None => res!(self.next_id()),
386 };
387 let new_window = res!(Window::new(
388 id.clone(),
389 cfg,
390 view,
391 None,
392 ));
393 let old = self.windows.insert(id.clone(), new_window);
394 res!(self.update_shared_window_list());
395 Ok((id, old.is_some()))
396 }
397
398 pub fn set_state(
399 &mut self,
400 id: &WindowId,
401 state: WindowStateInit,
402 )
403 -> Outcome<()>
404 {
405 let result = self.get_window_by_id_mut(&id);
406 let win = res!(result);
407 win.set_state(state);
408 Ok(())
409 }
410
411 pub fn add_text_box(
412 &mut self,
413 id: &WindowId,
414 ttbox: TabbedTextBox,
415 )
416 -> Outcome<()>
417 {
418 let result = self.get_window_by_id_mut(&id);
419 let win = res!(result);
420 match &mut win.state.text_boxes {
421 TextBoxesState::Single(_tbox) => {
422 return Err(err!(
423 "Cannot add text box to an existing single, untabbed container.";
424 Input, Invalid, Mismatch));
425 }
426 TextBoxesState::Tabbed(tmgr) => {
427 tmgr.tboxes.push(ttbox);
428 }
429 }
430 Ok(())
431 }
432
433 pub fn update_shared_window_list(&mut self) -> Outcome<()> {
434 // 1. Update the shared window list.
435 let list = self.list_windows();
436 let highlights = res!(HighlightBuilder::FullLine.build(list.clone()));
437 let mut window_list_text_lines = lock_write!(self.state.window_list);
438 let current_focus = if let Some(highlighter) = window_list_text_lines.get_highlighter() {
439 Some(highlighter.get_focus())
440 } else {
441 None
442 };
443 let highlighter = Highlighter::new(highlights, current_focus);
444 *window_list_text_lines = TextLines::new(
445 list.into_iter().map(|pair| pair.0).collect(),
446 Some(highlighter),
447 );
448 Ok(())
449 }
450
451 /// Returns a mutating iterator over the windows in reverse order, setting
452 /// the focus flag and mode for each as it goes.
453 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Window> {
454 let focus = &self.focus;
455 let mode = self.mode;
456 self.windows.iter_mut().rev().map(move |(id, window)| {
457 window.focus = *id == *focus;
458 window.mode = mode;
459 window
460 })
461 }
462
463 pub fn set_mode_update_menu(
464 &mut self,
465 mode: WindowMode,
466 menu_list_opt: Option<MenuList>,
467 )
468 -> Outcome<()>
469 {
470 self.mode = mode;
471 {
472 let mut text = lock_write!(self.state.menu_text);
473 if let Some(menu_list) = menu_list_opt {
474 *text = res!(menu_list.text_lines());
475 text.changed = true;
476 } else if let Some(text_lines) = self.menus.get(&mode) {
477 // Get the menu commands for the new mode, if not provided.
478 *text = text_lines.clone();
479 text.changed = true;
480 } else {
481 *text = TextLines::default();
482 text.changed = true;
483 }
484 }
485 if !self.windows.is_empty() {
486 let result = self.get_focal_window();
487 let win = res!(result);
488 let menu_text = win.cfg.menu_text.clone();
489 if let Some(menu_text) = menu_text {
490 // Add commands from the focal window to the menu.
491 let mut text = lock_write!(self.state.menu_text);
492 (*text).append_text_lines(menu_text);
493 }
494 }
495 // Add these commands to the end of all menu lists.
496 let mut text = lock_write!(self.state.menu_text);
497 let txt = "Exit Ironic (Ctlr + c) ";
498 (*text).append_text_line(
499 Text::new(txt, Some(TextType::MenuItem)),
500 Some(Highlight::new(
501 LineRange::new(0usize, (0usize, txt.len() - 1)),
502 0,
503 Some(HighlightType::Menu(Action::ExitApp)),
504 )),
505 );
506 Ok(())
507 }
508
509 pub fn next_focus(&mut self) {
510 let focus = &self.focus;
511 let mut range = self.windows.range(focus..);
512
513 if let Some((next_id, _)) = range.next() {
514 if next_id == focus {
515 if let Some((next_next_id, _)) = range.next() {
516 self.focus = next_next_id.clone();
517 } else if let Some((first_id, _)) = self.windows.iter().next() {
518 self.focus = first_id.clone();
519 }
520 } else {
521 self.focus = next_id.clone();
522 }
523 } else if let Some((first_id, _)) = self.windows.iter().next() {
524 self.focus = first_id.clone();
525 }
526 }
527
528 pub fn get_focus_id(&self) -> &WindowId {
529 &self.focus
530 }
531
532 /// Set the window focus by id.
533 pub fn set_focus_by_id(&mut self, id: &WindowId) -> Outcome<()> {
534 if self.windows.contains_key(id) {
535 self.focus = id.clone();
536 Ok(())
537 } else {
538 Err(err!(
539 "There is no window with id {:?}.", id;
540 Key, NotFound))
541 }
542 }
543
544 /// Set the window focus by label.
545 pub fn set_focus_by_label(&mut self, label: &str) -> Outcome<()> {
546 for (id, _window) in &self.windows {
547 if id.label() == label {
548 self.focus = id.clone();
549 return Ok(());
550 }
551 }
552 Err(err!(
553 "There is no window with label '{}'.", label;
554 Data, NotFound))
555 }
556
557 //pub fn list_windows(&self) -> Vec<(String, String)> {
558 // let mut result = Vec::new();
559 // for id in self.windows.keys() {
560 // result.push((id.title().to_string(), id.label().to_string()));
561 // }
562 // result
563 //}
564
565 pub fn list_windows(&self) -> Vec<(Text<TextType>, Option<HighlightType>)> {
566 let mut result = Vec::new();
567 for id in self.windows.keys() {
568 result.push((
569 Text::new(
570 fmt!("{:<10}{:20}", id.title(), id.label()),
571 Some(TextType::MenuItem),
572 ),
573 Some(HighlightType::Window(id.clone())),
574 ));
575 }
576 result
577 }
578
579 pub fn get_focal_window(&self) -> Outcome<&Window> {
580 if !self.windows.is_empty() {
581 if let Some(window) = self.windows.get(&self.focus) {
582 Ok(window)
583 } else {
584 Err(err!(
585 "There is no window corresponding to the focus id {:?}.", self.focus;
586 Bug, Key, NotFound))
587 }
588 } else {
589 Err(err!("There are no windows yet."; Bug, Data, Missing))
590 }
591 }
592
593 pub fn get_focal_window_mut(&mut self) -> Outcome<&mut Window> {
594 if !self.windows.is_empty() {
595 if let Some(window) = self.windows.get_mut(&self.focus) {
596 Ok(window)
597 } else {
598 Err(err!(
599 "There is no window corresponding to the focus id {:?}.", self.focus;
600 Bug, Key, NotFound))
601 }
602 } else {
603 Err(err!("There are no windows yet."; Bug, Data, Missing))
604 }
605 }
606
607 pub fn get_window_by_id(&self, id: &WindowId) -> Outcome<&Window> {
608 if let Some(window) = self.windows.get(id) {
609 Ok(window)
610 } else {
611 Err(err!(
612 "There is no window with id {:?}.", id;
613 Key, NotFound))
614 }
615 }
616
617 pub fn get_window_by_id_mut(&mut self, id: &WindowId) -> Outcome<&mut Window> {
618 if let Some(window) = self.windows.get_mut(id) {
619 Ok(window)
620 } else {
621 Err(err!(
622 "There is no window with id {:?}.", id;
623 Key, NotFound))
624 }
625 }
626
627 pub fn get_focal_window_text_box_mut(&mut self) -> Outcome<Option<&mut TextBox>> {
628 let result = self.get_focal_window_mut();
629 let window = res!(result);
630 Ok(window.get_focal_text_box_mut())
631 }
632
633 pub fn draw_cursor<S: Sink, R: Renderer<S>>(
634 &mut self,
635 drawer: &mut Drawer<S, R>,
636 when: When,
637 )
638 -> Outcome<()>
639 {
640 let result = self.get_focal_window_text_box_mut();
641 if let Some(tbox) = res!(result) {
642 match tbox.cfg.cursor_position {
643 PositionCursor::UserControlled | PositionCursor::LatestLine(true) => {
644 res!(drawer.rend.set_cursor(tbox.tview.term_cursor, when));
645 if let Some(cursor_style) = tbox.cfg.cursor_style {
646 res!(drawer.rend.show_cursor(when));
647 res!(drawer.rend.set_cursor_style(cursor_style, When::Later));
648 }
649 }
650 _ => {
651 res!(drawer.rend.hide_cursor(when));
652 }
653 }
654 }
655 Ok(())
656 }
657}