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