1 use std::collections::HashMap;
2 use std::time::{Duration, SystemTime};
3 
4 use cursive::align::HAlign;
5 use cursive::direction::Direction;
6 use cursive::event::{AnyCb, Event, EventResult};
7 use cursive::theme::{ColorStyle, ColorType, Theme};
8 use cursive::traits::View;
9 use cursive::vec::Vec2;
10 use cursive::view::{IntoBoxedView, Selector};
11 use cursive::views::EditView;
12 use cursive::{Cursive, Printer};
13 use log::debug;
14 use unicode_width::UnicodeWidthStr;
15 
16 use crate::command::Command;
17 use crate::commands::CommandResult;
18 use crate::events;
19 use crate::traits::{IntoBoxedViewExt, ViewExt};
20 
21 pub struct Layout {
22     screens: HashMap<String, Box<dyn ViewExt>>,
23     stack: HashMap<String, Vec<Box<dyn ViewExt>>>,
24     statusbar: Box<dyn View>,
25     focus: Option<String>,
26     pub cmdline: EditView,
27     cmdline_focus: bool,
28     result: Result<Option<String>, String>,
29     result_time: Option<SystemTime>,
30     screenchange: bool,
31     last_size: Vec2,
32     ev: events::EventManager,
33     theme: Theme,
34 }
35 
36 impl Layout {
new<T: IntoBoxedView>(status: T, ev: &events::EventManager, theme: Theme) -> Layout37     pub fn new<T: IntoBoxedView>(status: T, ev: &events::EventManager, theme: Theme) -> Layout {
38         let style = ColorStyle::new(
39             ColorType::Color(*theme.palette.custom("cmdline_bg").unwrap()),
40             ColorType::Color(*theme.palette.custom("cmdline").unwrap()),
41         );
42 
43         Layout {
44             screens: HashMap::new(),
45             stack: HashMap::new(),
46             statusbar: status.into_boxed_view(),
47             focus: None,
48             cmdline: EditView::new().filler(" ").style(style),
49             cmdline_focus: false,
50             result: Ok(None),
51             result_time: None,
52             screenchange: true,
53             last_size: Vec2::new(0, 0),
54             ev: ev.clone(),
55             theme,
56         }
57     }
58 
enable_cmdline(&mut self, prefix: char)59     pub fn enable_cmdline(&mut self, prefix: char) {
60         if !self.cmdline_focus {
61             self.cmdline.set_content(prefix);
62             self.cmdline_focus = true;
63         }
64     }
65 
enable_jump(&mut self)66     pub fn enable_jump(&mut self) {
67         if !self.cmdline_focus {
68             self.cmdline.set_content("/");
69             self.cmdline_focus = true;
70         }
71     }
72 
add_screen<S: Into<String>, T: IntoBoxedViewExt>(&mut self, id: S, view: T)73     pub fn add_screen<S: Into<String>, T: IntoBoxedViewExt>(&mut self, id: S, view: T) {
74         if let Some(view) = self.get_top_view() {
75             view.on_leave();
76         }
77 
78         let s = id.into();
79         self.screens.insert(s.clone(), view.into_boxed_view_ext());
80         self.stack.insert(s.clone(), Vec::new());
81         self.focus = Some(s);
82     }
83 
screen<S: Into<String>, T: IntoBoxedViewExt>(mut self, id: S, view: T) -> Self84     pub fn screen<S: Into<String>, T: IntoBoxedViewExt>(mut self, id: S, view: T) -> Self {
85         (&mut self).add_screen(id, view);
86         self
87     }
88 
has_screen(&self, id: &str) -> bool89     pub fn has_screen(&self, id: &str) -> bool {
90         self.screens.contains_key(id)
91     }
92 
set_screen<S: Into<String>>(&mut self, id: S)93     pub fn set_screen<S: Into<String>>(&mut self, id: S) {
94         if let Some(view) = self.get_top_view() {
95             view.on_leave();
96         }
97 
98         let s = id.into();
99         self.focus = Some(s);
100         self.cmdline_focus = false;
101         self.screenchange = true;
102 
103         // trigger a redraw
104         self.ev.trigger();
105     }
106 
set_result(&mut self, result: Result<Option<String>, String>)107     pub fn set_result(&mut self, result: Result<Option<String>, String>) {
108         self.result = result;
109         self.result_time = Some(SystemTime::now());
110     }
111 
clear_cmdline(&mut self)112     pub fn clear_cmdline(&mut self) {
113         self.cmdline.set_content("");
114         self.cmdline_focus = false;
115         self.result = Ok(None);
116         self.result_time = None;
117     }
118 
get_result(&self) -> Result<Option<String>, String>119     fn get_result(&self) -> Result<Option<String>, String> {
120         if let Some(t) = self.result_time {
121             if t.elapsed().unwrap() > Duration::from_secs(5) {
122                 return Ok(None);
123             }
124         }
125         self.result.clone()
126     }
127 
push_view(&mut self, view: Box<dyn ViewExt>)128     pub fn push_view(&mut self, view: Box<dyn ViewExt>) {
129         if let Some(view) = self.get_top_view() {
130             view.on_leave();
131         }
132 
133         if let Some(stack) = self.get_focussed_stack_mut() {
134             stack.push(view)
135         }
136     }
137 
pop_view(&mut self)138     pub fn pop_view(&mut self) {
139         if let Some(view) = self.get_top_view() {
140             view.on_leave();
141         }
142 
143         self.get_focussed_stack_mut().map(|stack| stack.pop());
144     }
145 
get_current_screen(&self) -> Option<&Box<dyn ViewExt>>146     fn get_current_screen(&self) -> Option<&Box<dyn ViewExt>> {
147         self.focus
148             .as_ref()
149             .and_then(|focus| self.screens.get(focus))
150     }
151 
get_focussed_stack_mut(&mut self) -> Option<&mut Vec<Box<dyn ViewExt>>>152     fn get_focussed_stack_mut(&mut self) -> Option<&mut Vec<Box<dyn ViewExt>>> {
153         let focus = self.focus.clone();
154         if let Some(focus) = &focus {
155             self.stack.get_mut(focus)
156         } else {
157             None
158         }
159     }
160 
get_focussed_stack(&self) -> Option<&Vec<Box<dyn ViewExt>>>161     fn get_focussed_stack(&self) -> Option<&Vec<Box<dyn ViewExt>>> {
162         self.focus.as_ref().and_then(|focus| self.stack.get(focus))
163     }
164 
get_top_view(&self) -> Option<&Box<dyn ViewExt>>165     fn get_top_view(&self) -> Option<&Box<dyn ViewExt>> {
166         let focussed_stack = self.get_focussed_stack();
167         if focussed_stack.map(|s| s.len()).unwrap_or_default() > 0 {
168             focussed_stack.unwrap().last()
169         } else if let Some(id) = &self.focus {
170             self.screens.get(id)
171         } else {
172             None
173         }
174     }
175 
get_current_view_mut(&mut self) -> Option<&mut Box<dyn ViewExt>>176     fn get_current_view_mut(&mut self) -> Option<&mut Box<dyn ViewExt>> {
177         if let Some(focus) = &self.focus {
178             let last_view = self
179                 .stack
180                 .get_mut(focus)
181                 .filter(|stack| !stack.is_empty())
182                 .and_then(|stack| stack.last_mut());
183             if last_view.is_some() {
184                 last_view
185             } else {
186                 self.screens.get_mut(focus)
187             }
188         } else {
189             None
190         }
191     }
192 }
193 
194 impl View for Layout {
draw(&self, printer: &Printer<'_, '_>)195     fn draw(&self, printer: &Printer<'_, '_>) {
196         let result = self.get_result();
197 
198         let cmdline_visible = self.cmdline.get_content().len() > 0;
199         let mut cmdline_height = if cmdline_visible { 1 } else { 0 };
200         if result.as_ref().map(Option::is_some).unwrap_or(true) {
201             cmdline_height += 1;
202         }
203 
204         let screen_title = self
205             .get_current_screen()
206             .map(|screen| screen.title())
207             .unwrap_or_default();
208 
209         if let Some(view) = self.get_top_view() {
210             // back button + title
211             if !self
212                 .get_focussed_stack()
213                 .map(|s| s.is_empty())
214                 .unwrap_or(false)
215             {
216                 printer.with_color(ColorStyle::title_secondary(), |printer| {
217                     printer.print((1, 0), &format!("< {}", screen_title));
218                 });
219             }
220 
221             // view title
222             printer.with_color(ColorStyle::title_primary(), |printer| {
223                 let offset = HAlign::Center.get_offset(view.title().width(), printer.size.x);
224                 printer.print((offset, 0), &view.title());
225             });
226 
227             printer.with_color(ColorStyle::secondary(), |printer| {
228                 let offset = HAlign::Right.get_offset(view.title_sub().width(), printer.size.x);
229                 printer.print((offset, 0), &view.title_sub());
230             });
231 
232             // screen content
233             let printer = &printer
234                 .offset((0, 1))
235                 .cropped((printer.size.x, printer.size.y - 3 - cmdline_height))
236                 .focused(true);
237             view.draw(printer);
238         }
239 
240         self.statusbar
241             .draw(&printer.offset((0, printer.size.y - 2 - cmdline_height)));
242 
243         if let Ok(Some(r)) = result {
244             printer.print_hline((0, printer.size.y - cmdline_height), printer.size.x, " ");
245             printer.print((0, printer.size.y - cmdline_height), &r);
246         } else if let Err(e) = result {
247             let style = ColorStyle::new(
248                 ColorType::Color(*self.theme.palette.custom("error").unwrap()),
249                 ColorType::Color(*self.theme.palette.custom("error_bg").unwrap()),
250             );
251 
252             printer.with_color(style, |printer| {
253                 printer.print_hline((0, printer.size.y - cmdline_height), printer.size.x, " ");
254                 printer.print(
255                     (0, printer.size.y - cmdline_height),
256                     &format!("ERROR: {}", e),
257                 );
258             });
259         }
260 
261         if cmdline_visible {
262             let printer = &printer.offset((0, printer.size.y - 1));
263             self.cmdline.draw(printer);
264         }
265     }
266 
layout(&mut self, size: Vec2)267     fn layout(&mut self, size: Vec2) {
268         self.last_size = size;
269 
270         self.statusbar.layout(Vec2::new(size.x, 2));
271 
272         self.cmdline.layout(Vec2::new(size.x, 1));
273 
274         if let Some(view) = self.get_current_view_mut() {
275             view.layout(Vec2::new(size.x, size.y - 3));
276         }
277 
278         // the focus view has changed, let the views know so they can redraw
279         // their items
280         if self.screenchange {
281             debug!("layout: new screen selected: {:?}", self.focus);
282             self.screenchange = false;
283         }
284     }
285 
required_size(&mut self, constraint: Vec2) -> Vec2286     fn required_size(&mut self, constraint: Vec2) -> Vec2 {
287         Vec2::new(constraint.x, constraint.y)
288     }
289 
on_event(&mut self, event: Event) -> EventResult290     fn on_event(&mut self, event: Event) -> EventResult {
291         if let Event::Mouse { position, .. } = event {
292             let result = self.get_result();
293 
294             let cmdline_visible = self.cmdline.get_content().len() > 0;
295             let mut cmdline_height = if cmdline_visible { 1 } else { 0 };
296             if result.as_ref().map(Option::is_some).unwrap_or(true) {
297                 cmdline_height += 1;
298             }
299 
300             if position.y < self.last_size.y.saturating_sub(2 + cmdline_height) {
301                 if let Some(view) = self.get_current_view_mut() {
302                     view.on_event(event);
303                 }
304             } else if position.y < self.last_size.y - cmdline_height {
305                 self.statusbar.on_event(
306                     event.relativized(Vec2::new(0, self.last_size.y - 2 - cmdline_height)),
307                 );
308             }
309 
310             return EventResult::Consumed(None);
311         }
312 
313         if self.cmdline_focus {
314             return self.cmdline.on_event(event);
315         }
316 
317         if let Some(view) = self.get_current_view_mut() {
318             view.on_event(event)
319         } else {
320             EventResult::Ignored
321         }
322     }
323 
call_on_any<'a>(&mut self, s: &Selector, c: AnyCb<'a>)324     fn call_on_any<'a>(&mut self, s: &Selector, c: AnyCb<'a>) {
325         if let Some(view) = self.get_current_view_mut() {
326             view.call_on_any(s, c);
327         }
328     }
329 
take_focus(&mut self, source: Direction) -> bool330     fn take_focus(&mut self, source: Direction) -> bool {
331         if self.cmdline_focus {
332             return self.cmdline.take_focus(source);
333         }
334 
335         if let Some(view) = self.get_current_view_mut() {
336             view.take_focus(source)
337         } else {
338             false
339         }
340     }
341 }
342 
343 impl ViewExt for Layout {
on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String>344     fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
345         match cmd {
346             Command::Focus(view) => {
347                 // Clear search results and return to search bar
348                 // If trying to focus search screen while already on it
349                 let search_view_name = "search";
350                 if view == search_view_name && self.focus == Some(search_view_name.into()) {
351                     if let Some(stack) = self.stack.get_mut(search_view_name) {
352                         stack.clear();
353                     }
354                 }
355 
356                 if self.screens.keys().any(|k| k == view) {
357                     self.set_screen(view.clone());
358                     let screen = self.screens.get_mut(view).unwrap();
359                     screen.on_command(s, cmd)?;
360                 }
361 
362                 Ok(CommandResult::Consumed(None))
363             }
364             Command::Back => {
365                 self.pop_view();
366                 Ok(CommandResult::Consumed(None))
367             }
368             _ => {
369                 if let Some(view) = self.get_current_view_mut() {
370                     view.on_command(s, cmd)
371                 } else {
372                     Ok(CommandResult::Ignored)
373                 }
374             }
375         }
376     }
377 }
378