diff options
Diffstat (limited to 'src/state.rs')
-rw-r--r-- | src/state.rs | 355 |
1 files changed, 355 insertions, 0 deletions
diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..dbce7b9 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,355 @@ +/* + * bb + * + * Copyright 2019 Manos Pitsidianakis + * + * This file is part of bb. + * + * bb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * bb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with bb. If not, see <http://www.gnu.org/licenses/>. + */ + +/*! The application's state. + +The UI crate has an Box<dyn Component>-Component-System design. The System part, is also the application's state, so they're both merged in the `State` struct. + +`State` owns all the Components of the UI. In the application's main event loop, input is handed to the state in the form of `UIEvent` objects which traverse the component graph. Components decide to handle each input or not. + +Input is received in the main loop from threads which listen on the stdin for user input, observe folders for file changes etc. The relevant struct is `ThreadEvent`. +*/ + +use super::*; +use crossbeam::channel::{Receiver, Sender}; +use std::collections::VecDeque; +use std::io::Write; + +use termion::raw::IntoRawMode; +use termion::screen::AlternateScreen; +use termion::{clear, cursor}; + +#[derive(PartialEq)] +pub enum UIMode { + Normal, + Input, +} + +pub type StateStdout = termion::screen::AlternateScreen<termion::raw::RawTerminal<std::io::Stdout>>; + +struct InputHandler { + rx: Receiver<bool>, + tx: Sender<bool>, +} + +impl InputHandler { + fn restore(&self, tx: Sender<ThreadEvent>) { + let stdin = std::io::stdin(); + let rx = self.rx.clone(); + std::thread::Builder::new() + .name("input-thread".to_string()) + .spawn(move || { + get_events( + stdin, + |k| { + tx.send(ThreadEvent::Input(k)).unwrap(); + }, + &rx, + ) + }) + .unwrap(); + } + fn kill(&self) { + self.tx.send(false).unwrap(); + } +} + +/// A State object to manage and own components and components of the UI. `State` is responsible for +/// managing the terminal +pub struct UIState { + cols: usize, + rows: usize, + + grid: CellBuffer, + stdout: Option<StateStdout>, + components: Vec<Box<dyn Component>>, + pub dirty_areas: VecDeque<Area>, + sender: Sender<ThreadEvent>, + receiver: Receiver<ThreadEvent>, + input: InputHandler, + pub mode: UIMode, +} + +impl Drop for UIState { + fn drop(&mut self) { + // When done, restore the defaults to avoid messing with the terminal. + self.switch_to_main_screen(); + } +} + +impl Default for UIState { + fn default() -> Self { + Self::new() + } +} + +impl UIState { + pub fn new() -> Self { + /* Create a channel to communicate with other threads. The main process is the sole receiver. + * */ + let (sender, receiver) = + crossbeam::channel::bounded(32 * ::std::mem::size_of::<ThreadEvent>()); + + /* + * Create async channel to block the input-thread if we need to fork and stop it from reading + * stdin, see get_events() for details + * */ + let (input_sender, input_receiver) = crossbeam::channel::unbounded(); + + let termsize = termion::terminal_size().ok(); + let cols = termsize.map(|(w, _)| w).unwrap_or(0) as usize; + let rows = termsize.map(|(_, h)| h).unwrap_or(0) as usize; + + let _stdout = std::io::stdout(); + _stdout.lock(); + let stdout = AlternateScreen::from(_stdout.into_raw_mode().unwrap()); + + let mut s = UIState { + cols, + rows, + grid: CellBuffer::new(cols, rows, Cell::with_char(' ')), + stdout: Some(stdout), + components: Vec::with_capacity(1), + sender, + receiver, + dirty_areas: VecDeque::new(), + input: InputHandler { + rx: input_receiver, + tx: input_sender, + }, + mode: UIMode::Normal, + }; + + write!( + s.stdout(), + "{}{}{}{}", + BracketModeStart, + cursor::Hide, + clear::All, + cursor::Goto(1, 1) + ) + .unwrap(); + s.flush(); + s.restore_input(); + s + } + + /// Switch back to the terminal's main screen (The command line the user sees before opening + /// the application) + pub fn switch_to_main_screen(&mut self) { + write!( + self.stdout(), + "{}{}{}", + termion::screen::ToMainScreen, + cursor::Show, + BracketModeEnd, + ) + .unwrap(); + self.flush(); + self.stdout = None; + self.input.kill(); + } + pub fn switch_to_alternate_screen(&mut self) { + let s = std::io::stdout(); + s.lock(); + self.stdout = Some(AlternateScreen::from(s.into_raw_mode().unwrap())); + + write!( + self.stdout(), + "{}{}{}{}{}", + termion::screen::ToAlternateScreen, + cursor::Hide, + clear::All, + cursor::Goto(1, 1), + BracketModeStart, + ) + .unwrap(); + self.flush(); + } + + pub fn receiver(&self) -> Receiver<ThreadEvent> { + self.receiver.clone() + } + + /// On `SIGWNICH` the `State` redraws itself according to the new terminal size. + pub fn update_size(&mut self) { + let termsize = termion::terminal_size().ok(); + let termcols = termsize.map(|(w, _)| w); + let termrows = termsize.map(|(_, h)| h); + if termcols.unwrap_or(72) as usize != self.cols + || termrows.unwrap_or(120) as usize != self.rows + { + eprintln!( + "Size updated, from ({}, {}) -> ({:?}, {:?})", + self.cols, self.rows, termcols, termrows + ); + } + self.cols = termcols.unwrap_or(72) as usize; + self.rows = termrows.unwrap_or(120) as usize; + self.grid.resize(self.cols, self.rows, Cell::with_char(' ')); + + self.rcv_event(UIEvent::Resize); + + // Invalidate dirty areas. + self.dirty_areas.clear(); + } + + /// Force a redraw for all dirty components. + pub fn redraw(&mut self, tick: bool) { + for i in 0..self.components.len() { + self.draw_component(i, tick); + } + let mut areas: Vec<Area> = self.dirty_areas.drain(0..).collect(); + /* Sort by x_start, ie upper_left corner's x coordinate */ + areas.sort_by(|a, b| (a.0).0.partial_cmp(&(b.0).0).unwrap()); + /* draw each dirty area */ + let rows = self.rows; + for y in 0..rows { + let mut segment = None; + for ((x_start, y_start), (x_end, y_end)) in &areas { + if y < *y_start || y > *y_end { + continue; + } + if let Some((x_start, x_end)) = segment.take() { + self.draw_horizontal_segment(x_start, x_end, y); + } + match segment { + ref mut s @ None => { + *s = Some((*x_start, *x_end)); + } + ref mut s @ Some(_) if s.unwrap().1 < *x_start => { + self.draw_horizontal_segment(s.unwrap().0, s.unwrap().1, y); + *s = Some((*x_start, *x_end)); + } + ref mut s @ Some(_) if s.unwrap().1 < *x_end => { + self.draw_horizontal_segment(s.unwrap().0, s.unwrap().1, y); + *s = Some((s.unwrap().1, *x_end)); + } + Some((_, ref mut x)) => { + *x = *x_end; + } + } + } + if let Some((x_start, x_end)) = segment { + self.draw_horizontal_segment(x_start, x_end, y); + } + } + self.flush(); + } + + /// Draw only a specific `area` on the screen. + fn draw_horizontal_segment(&mut self, x_start: usize, x_end: usize, y: usize) { + write!( + self.stdout(), + "{}", + cursor::Goto(x_start as u16 + 1, (y + 1) as u16) + ) + .unwrap(); + for x in x_start..=x_end { + let c = self.grid[(x, y)]; + if c.bg() != Color::Default { + write!(self.stdout(), "{}", termion::color::Bg(c.bg().as_termion())).unwrap(); + } + if c.fg() != Color::Default { + write!(self.stdout(), "{}", termion::color::Fg(c.fg().as_termion())).unwrap(); + } + if c.attrs() != Attr::Default { + write!(self.stdout(), "\x1B[{}m", c.attrs() as u8).unwrap(); + } + if !c.empty() { + write!(self.stdout(), "{}", c.ch()).unwrap(); + } + + if c.bg() != Color::Default { + write!( + self.stdout(), + "{}", + termion::color::Bg(termion::color::Reset) + ) + .unwrap(); + } + if c.fg() != Color::Default { + write!( + self.stdout(), + "{}", + termion::color::Fg(termion::color::Reset) + ) + .unwrap(); + } + if c.attrs() != Attr::Default { + write!(self.stdout(), "\x1B[{}m", Attr::Default as u8).unwrap(); + } + } + } + + /// Draw the entire screen from scratch. + pub fn render(&mut self) { + self.update_size(); + let cols = self.cols; + let rows = self.rows; + self.dirty_areas.push_back(((0, 0), (cols - 1, rows - 1))); + + self.redraw(true); + } + + pub fn draw_component(&mut self, idx: usize, tick: bool) { + if self.cols < 80 || self.cols < 24 { + return; + } + + let component = &mut self.components[idx]; + let upper_left = (0, 0); + let bottom_right = (self.cols - 1, self.rows - 1); + + if component.is_dirty() { + component.draw( + &mut self.grid, + (upper_left, bottom_right), + &mut self.dirty_areas, + tick, + ); + } + } + pub fn register_component(&mut self, component: Box<dyn Component>) { + self.components.push(component); + } + /// The application's main loop sends `UIEvents` to state via this method. + pub fn rcv_event(&mut self, mut event: UIEvent) { + /* inform each component */ + for i in 0..self.components.len() { + self.components[i].process_event(&mut event, &mut self.mode); + } + } + + fn flush(&mut self) { + if let Some(s) = self.stdout.as_mut() { + s.flush().unwrap(); + } + } + + fn stdout(&mut self) -> &mut StateStdout { + self.stdout.as_mut().unwrap() + } + + pub fn restore_input(&self) { + self.input.restore(self.sender.clone()); + } +} |