1use 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
19struct 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
33pub struct Screen {
35 out: Stdout,
37 dim: Dim,
39 theme: Theme,
41 style: Option<TextStyle>,
43 keymap: KeyMap,
45 ev_stream: EvStreamFut,
47}
48
49impl Screen {
50 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 pub fn set_keymap(&mut self, keymap: KeyMap) {
80 self.keymap = keymap;
81 }
82
83 pub fn set_title(&mut self, title: &str) -> Result<()> {
85 queue!(self.out, terminal::SetTitle(title))?;
86 Ok(())
87 }
88
89 pub fn set_theme(&mut self, theme: Theme) {
91 self.theme = theme;
92 }
93
94 fn bbox(&self) -> BBox {
96 BBox::new(0, 0, self.dim.width, self.dim.height)
97 }
98
99 pub(crate) fn theme(&self) -> &Theme {
101 &self.theme
102 }
103
104 fn clear(&mut self) -> Result<()> {
106 queue!(self.out, terminal::Clear(terminal::ClearType::All))?;
107 Ok(())
108 }
109
110 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 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 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 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 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 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 pub(crate) fn move_right(&mut self, col: u16) -> Result<()> {
164 queue!(self.out, cursor::MoveRight(col))?;
165 Ok(())
166 }
167
168 pub(crate) fn print_char(&mut self, ch: char) -> Result<()> {
170 queue!(self.out, style::Print(ch))?;
171 Ok(())
172 }
173
174 pub(crate) fn print_str(&mut self, st: &str) -> Result<()> {
176 queue!(self.out, style::Print(st))?;
177 Ok(())
178 }
179
180 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 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 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 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 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 dbg!(err);
251 }
252 }
253}
254
255fn 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 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}