semtext/
screen.rs

1// screen.rs
2//
3// Copyright (c) 2020  Douglas P Lau
4//
5use crate::input::{Action, Event, FocusEvent, KeyMap, ModKeys, MouseEvent};
6use crate::layout::{BBox, Cells, Dim, GridArea, Pos};
7use crate::text::{Appearance, Color, StyleGroup, TextStyle, Theme};
8use crate::{Result, Widget};
9use crossterm::event::Event as CtEvent;
10use crossterm::{cursor, event, queue, style, terminal};
11use futures_core::stream::Stream;
12use std::io::{Stdout, Write};
13use std::{
14    future::Future,
15    pin::Pin,
16    task::{Context, Poll},
17};
18
19/// Needed in order to await the stream.
20struct EvStreamFut(Box<dyn Stream<Item = crossterm::Result<CtEvent>> + Unpin>);
21
22impl Future for EvStreamFut {
23    type Output = Option<crossterm::Result<CtEvent>>;
24
25    fn poll(
26        mut self: Pin<&mut Self>,
27        cx: &mut Context<'_>,
28    ) -> Poll<Self::Output> {
29        Pin::new(&mut self.0).poll_next(cx)
30    }
31}
32
33/// Terminal screen
34pub struct Screen {
35    /// Standard Output
36    out: Stdout,
37    /// Dimensions of screen in text cells
38    dim: Dim,
39    /// Style theme
40    theme: Theme,
41    /// Current text style
42    style: Option<TextStyle>,
43    /// Key / action map
44    keymap: KeyMap,
45    /// Event stream future.
46    ev_stream: EvStreamFut,
47}
48
49impl Screen {
50    /// Create a new Screen
51    pub fn new() -> Result<Self> {
52        let (width, height) = terminal::size()?;
53        let dim = Dim::new(width, height);
54        let theme = Theme::default();
55        let style = None;
56        let keymap = KeyMap::default();
57        terminal::enable_raw_mode()?;
58        let mut out = std::io::stdout();
59        queue!(
60            out,
61            terminal::EnterAlternateScreen,
62            cursor::Hide,
63            terminal::DisableLineWrap,
64            terminal::Clear(terminal::ClearType::All),
65            event::EnableMouseCapture,
66        )?;
67        let ev_stream = EvStreamFut(Box::new(event::EventStream::new()));
68        Ok(Screen {
69            out,
70            dim,
71            theme,
72            style,
73            keymap,
74            ev_stream,
75        })
76    }
77
78    /// Set the key / action map
79    pub fn set_keymap(&mut self, keymap: KeyMap) {
80        self.keymap = keymap;
81    }
82
83    /// Set the screen title
84    pub fn set_title(&mut self, title: &str) -> Result<()> {
85        queue!(self.out, terminal::SetTitle(title))?;
86        Ok(())
87    }
88
89    /// Set the theme
90    pub fn set_theme(&mut self, theme: Theme) {
91        self.theme = theme;
92    }
93
94    /// Get the screen bounding box
95    fn bbox(&self) -> BBox {
96        BBox::new(0, 0, self.dim.width, self.dim.height)
97    }
98
99    /// Get the theme
100    pub(crate) fn theme(&self) -> &Theme {
101        &self.theme
102    }
103
104    /// Clear the screen (fill with the space character)
105    fn clear(&mut self) -> Result<()> {
106        queue!(self.out, terminal::Clear(terminal::ClearType::All))?;
107        Ok(())
108    }
109
110    /// Get cells contained by a bounding box
111    fn cells(&mut self, bbox: BBox) -> Option<Cells> {
112        let bbox = self.bbox().clip(bbox);
113        if bbox.dim().is_empty() {
114            None
115        } else {
116            Some(Cells::new(self, bbox))
117        }
118    }
119
120    /// Set the background color
121    fn set_background_color(&mut self, color: Color) -> Result<()> {
122        if self.style.map_or(true, |s| s.background() != color) {
123            queue!(self.out, style::SetBackgroundColor(color.into()))?;
124        }
125        Ok(())
126    }
127
128    /// Set the foreground color
129    fn set_foreground_color(&mut self, color: Color) -> Result<()> {
130        if self.style.map_or(true, |s| s.foreground() != color) {
131            queue!(self.out, style::SetForegroundColor(color.into()))?;
132        }
133        Ok(())
134    }
135
136    /// Set the text appearance
137    fn set_appearance(&mut self, app: Appearance) -> Result<()> {
138        let attrs = app.changed(
139            self.style.map_or(Appearance::default(), |s| s.appearance()),
140        );
141        if !attrs.is_empty() {
142            queue!(self.out, style::SetAttributes(attrs))?;
143        }
144        Ok(())
145    }
146
147    /// Set the text style
148    pub(crate) fn set_style(&mut self, st: TextStyle) -> Result<()> {
149        self.set_background_color(st.background())?;
150        self.set_foreground_color(st.foreground())?;
151        self.set_appearance(st.appearance())?;
152        self.style = Some(st);
153        Ok(())
154    }
155
156    /// Move cursor to a cell
157    pub(crate) fn move_to(&mut self, col: u16, row: u16) -> Result<()> {
158        queue!(self.out, cursor::MoveTo(col, row))?;
159        Ok(())
160    }
161
162    /// Move cursor right by a number of columns
163    pub(crate) fn move_right(&mut self, col: u16) -> Result<()> {
164        queue!(self.out, cursor::MoveRight(col))?;
165        Ok(())
166    }
167
168    /// Print a char at the cursor location
169    pub(crate) fn print_char(&mut self, ch: char) -> Result<()> {
170        queue!(self.out, style::Print(ch))?;
171        Ok(())
172    }
173
174    /// Print a str at the cursor location
175    pub(crate) fn print_str(&mut self, st: &str) -> Result<()> {
176        queue!(self.out, style::Print(st))?;
177        Ok(())
178    }
179
180    /// Draw a grid area layout
181    fn draw(&mut self, widget_boxes: &[(&dyn Widget, BBox)]) -> Result<()> {
182        let pos = Pos::default();
183        let style = self.theme.style(StyleGroup::Enabled);
184        self.set_style(style)?;
185        self.clear()?;
186        for (widget, bbox) in widget_boxes.iter() {
187            if let Some(mut cells) = self.cells(*bbox) {
188                let style = cells.theme().style(widget.style_group());
189                cells.set_style(style)?;
190                widget.draw(&mut cells, pos)?;
191            }
192        }
193        self.out.flush()?;
194        Ok(())
195    }
196
197    /// Check an event for an action
198    fn event_action(
199        &mut self,
200        ev: Event,
201        widget_boxes: &[(&dyn Widget, BBox)],
202    ) -> Option<Action> {
203        match ev {
204            Event::Resize(dim) => {
205                self.dim = dim;
206                Some(Action::Resize(dim))
207            }
208            Event::Key(key, mods) => {
209                // FIXME: check focused widget first
210                self.keymap.lookup(key, mods)
211            }
212            Event::Mouse(mev, mods, pos) => {
213                mouse_action(mev, mods, pos, widget_boxes)
214            }
215        }
216    }
217
218    /// Render a grid area and wait asynchronously for an action
219    pub async fn step(&mut self, area: &GridArea<'_>) -> Result<Action> {
220        let widget_boxes = area.widget_boxes(self.bbox(), &self.theme);
221        self.draw(&widget_boxes)?;
222        loop {
223            let ev = (&mut self.ev_stream).await.unwrap()?.into();
224            if let Some(action) = self.event_action(ev, &widget_boxes) {
225                return Ok(action);
226            }
227        }
228    }
229
230    /// Cleanup screen
231    fn cleanup(&mut self) -> Result<()> {
232        queue!(
233            self.out,
234            event::DisableMouseCapture,
235            terminal::LeaveAlternateScreen,
236            terminal::EnableLineWrap,
237            cursor::Show,
238            style::ResetColor,
239        )?;
240        self.out.flush()?;
241        terminal::disable_raw_mode()?;
242        Ok(())
243    }
244}
245
246impl Drop for Screen {
247    fn drop(&mut self) {
248        if let Err(err) = self.cleanup() {
249            // Is this useful?
250            dbg!(err);
251        }
252    }
253}
254
255/// Handle a mouse action
256fn mouse_action(
257    mev: MouseEvent,
258    mods: ModKeys,
259    pos: Pos,
260    widget_boxes: &[(&dyn Widget, BBox)],
261) -> Option<Action> {
262    let mut action = None;
263    let mut redraw = None;
264    for (widget, bbox) in widget_boxes.iter() {
265        use MouseEvent::*;
266        let r = match (mev, bbox.within(pos)) {
267            (ButtonDown(_), Some(_)) => widget.focus(FocusEvent::Offer),
268            (ButtonDown(_), None) => widget.focus(FocusEvent::Take),
269            (Drag(None), Some(_)) => widget.focus(FocusEvent::HoverInside),
270            (Drag(_), None) => widget.focus(FocusEvent::HoverOutside),
271            (ButtonUp(_), Some(_)) => widget.focus(FocusEvent::HoverInside),
272            (ButtonUp(_), None) => widget.focus(FocusEvent::HoverOutside),
273            _ => None,
274        };
275        redraw = redraw.or(r);
276        // Only widget within bounds receives event
277        if let Some(p) = bbox.within(pos) {
278            let a = widget.mouse_event(mev, mods, bbox.dim(), p);
279            action = action.or(a);
280        }
281    }
282    action.or(redraw)
283}