wez / wezterm

A GPU-accelerated cross-platform terminal emulator and multiplexer written by @wez and implemented in Rust
https://wezfurlong.org/wezterm/
Other
15.24k stars 687 forks source link

Add example for nested multiple widget usage #171

Open prabirshrestha opened 4 years ago

prabirshrestha commented 4 years ago

Here is sort of an example I was thinking off. When would one add or remove the widget.

<Editor>
    <Buffer />
    <StatusLine />
</Editor>
archseer commented 4 years ago

I've had no luck with nested widgets either. Adding a blank MainScreen and then embedding the Buffer as a child will work, but drastically slow down rendering with visible flashes as mouse events propagate. If I switch it back to being the root, it works fine. I guess it somehow forces the UI to continually redraw even though there was no change.

I also tried to add a 100% width, height 1 status line, together with a 100%/100% buffer but it messes up the rendering, the buffer becomes 1 char wide and continually flashes. I saw there was a child direction field there (and also valign) but neither seemed to work for me.

Code sample ```rust //! This example shows how to make a basic widget that accumulates //! text input and renders it to the screen #![allow(unused)] use anyhow::Error; use termwiz::caps::Capabilities; use termwiz::cell::AttributeChange; use termwiz::color::{AnsiColor, ColorAttribute, RgbColor}; use termwiz::input::*; use termwiz::surface::Change; use termwiz::terminal::buffered::BufferedTerminal; use termwiz::terminal::{new_terminal, Terminal}; #[cfg(feature = "widgets")] use termwiz::widgets::*; /// This is a widget for our application struct MainScreen<'a> { } impl<'a> MainScreen<'a> { pub fn new() -> Self { Self { } } } #[cfg(feature = "widgets")] impl<'a> Widget for MainScreen<'a> { fn process_event(&mut self, event: &WidgetEvent, _args: &mut UpdateArgs) -> bool { true // handled it all } /// Draw ourselves into the surface provided by RenderArgs fn render(&mut self, args: &mut RenderArgs) { } fn get_size_constraints(&self) -> layout::Constraints { layout::Constraints::default() } } /// This is a widget for our application struct Buffer<'a> { /// Holds the input text that we wish the widget to display text: &'a mut String, } impl<'a> Buffer<'a> { /// Initialize the widget with the input text pub fn new(text: &'a mut String) -> Self { Self { text } } } #[cfg(feature = "widgets")] impl<'a> Widget for Buffer<'a> { fn process_event(&mut self, event: &WidgetEvent, _args: &mut UpdateArgs) -> bool { match event { WidgetEvent::Input(InputEvent::Key(KeyEvent { key: KeyCode::Char(c), .. })) => self.text.push(*c), WidgetEvent::Input(InputEvent::Key(KeyEvent { key: KeyCode::Enter, .. })) => { self.text.push_str("\r\n"); } WidgetEvent::Input(InputEvent::Paste(s)) => { self.text.push_str(&s); } _ => {} } true // handled it all } /// Draw ourselves into the surface provided by RenderArgs fn render(&mut self, args: &mut RenderArgs) { args.surface.add_change(Change::ClearScreen( ColorAttribute::TrueColorWithPaletteFallback( RgbColor::new(0x31, 0x1B, 0x92), AnsiColor::Black.into(), ), )); args.surface .add_change(Change::Attribute(AttributeChange::Foreground( ColorAttribute::TrueColorWithPaletteFallback( RgbColor::new(0xB3, 0x88, 0xFF), AnsiColor::Purple.into(), ), ))); let dims = args.surface.dimensions(); args.surface .add_change(format!("🤷 surface size is {:?}\r\n", dims)); args.surface.add_change(self.text.clone()); // Place the cursor at the end of the text. // A more advanced text editing widget would manage the // cursor position differently. *args.cursor = CursorShapeAndPosition { coords: args.surface.cursor_position().into(), shape: termwiz::surface::CursorShape::SteadyBar, ..Default::default() }; } fn get_size_constraints(&self) -> layout::Constraints { layout::Constraints::default() } } /// This is a widget for our application struct MainScreen<'a> { /// Holds the input text that we wish the widget to display text: &'a mut String, } impl<'a> MainScreen<'a> { /// Initialize the widget with the input text pub fn new(text: &'a mut String) -> Self { Self { text } } } #[cfg(feature = "widgets")] impl<'a> Widget for StatusLine<'a> { fn process_event(&mut self, event: &WidgetEvent, _args: &mut UpdateArgs) -> bool { true // handled it all } /// Draw ourselves into the surface provided by RenderArgs fn render(&mut self, args: &mut RenderArgs) { args.surface.add_change(Change::ClearScreen( ColorAttribute::TrueColorWithPaletteFallback( RgbColor::new(0xFF, 0xFF, 0xFF), AnsiColor::Black.into(), ), )); } fn get_size_constraints(&self) -> layout::Constraints { *layout::Constraints::default().set_fixed_height(1) } } #[cfg(feature = "widgets")] fn main() -> Result<(), Error> { // Start with an empty string; typing into the app will // update this string. let mut typed_text = String::new(); { // Create a terminal and put it into full screen raw mode let caps = Capabilities::new_from_env()?; let mut buf = BufferedTerminal::new(new_terminal(caps)?)?; buf.terminal().set_raw_mode()?; // Set up the UI let mut ui = Ui::new(); let root_id = ui.set_root(MainScreen::new()); ui.add_widget(root_id, Buffer::new(&mut typed_text)); // ui.add_widget(root_id, StatusLine::new()); loop { ui.process_event_queue()?; // After updating and processing all of the widgets, compose them // and render them to the screen. if ui.render_to_screen(&mut buf)? { // We have more events to process immediately; don't block waiting // for input below, but jump to the top of the loop to re-run the // updates. continue; } // Compute an optimized delta to apply to the terminal and display it buf.flush()?; // Wait for user input match buf.terminal().poll_input(None) { Ok(Some(InputEvent::Resized { rows, cols })) => { // FIXME: this is working around a bug where we don't realize // that we should redraw everything on resize in BufferedTerminal. buf.add_change(Change::ClearScreen(Default::default())); buf.resize(cols, rows); } Ok(Some(input)) => match input { InputEvent::Key(KeyEvent { key: KeyCode::Escape, .. }) => { // Quit the app when escape is pressed break; } input @ _ => { // Feed input into the Ui ui.queue_event(WidgetEvent::Input(input)); } }, Ok(None) => {} Err(e) => { print!("{:?}\r\n", e); break; } } } } // After we've stopped the full screen raw terminal, // print out the final edited value of the input text. println!("The text you entered: {}", typed_text); Ok(()) } #[cfg(not(feature = "widgets"))] fn main() { println!("recompile with --features widgets"); } ```
archseer commented 4 years ago

(I think the layout system could potentially be more flexible if we embedded https://github.com/vislyhq/stretch which is built on top of cassowary, instead of cassowary directly)

wez commented 4 years ago

@archseer thanks for looking at this!

I took your example and used it to debug things and found a couple of issues:

I've tweaked your example and added it to the repo; don't forget to run it in release mode!:

cd termwiz
cargo run --example widgets_nested --features widgets --release
archseer commented 4 years ago

there was some missing optimization for nested widgets that meant that the whole screen would be re-rendered on each pass; I've added a surface to absorb those changes and then compute a diff to improve the render performance.

Makes sense, I thought it had to be something like that. Thanks for the fix!

Also, I noticed that the layout gets recalculated on every render_to_screen call: https://github.com/wez/wezterm/blob/81683bd898f5059ff453f52c4a7be3a88deeb7b2/termwiz/src/widgets/mod.rs#L456-L457

Since this is already handled by the resize event handler, I think you should be able to remove that call as well, as long as it's called once on init (I noticed the surfaces default to (1, 1)).

wez commented 4 years ago

The recompute on render is deliberate: the rationale is that a widget's size may have changed to fit text that got updated. It may be possible to make that more optimal though!

archseer commented 4 years ago

Cool, I played around with it a bit today and it's quite neat! I had some more thoughts about nested widgets, for implementing something like a popup it would be useful to:

archseer commented 4 years ago

Segfault on resize if the terminal is resized to a smaller area:

thread 'main' panicked at 'assertion failed: x + width <= self.width', ~/.local/share/cargo/git/checkouts/wezterm-6425bab852909cc8/58686f9/termwiz/src/surface/mod.rs:731:9                                                                                                          

Seems to happen on a single widget too.

(I hope you don't mind me reusing this same issue, I figured might as well put all nested widget issues here as I find them)

prabirshrestha commented 4 years ago

Even I get panic during resize.

Another example I wanted to actually see was how to dynamically add or remove widget. Might be have a ctrl-b that shows and hides status bar?

Another option for widgets is similar to yew or iced or vgtk.

archseer commented 4 years ago

Removing widgets: I think there's no bindings for that at the moment. There's only add which is proxied by add_child/add_root.

prabirshrestha commented 4 years ago

In the meantime I sent a PR for tui-rs https://github.com/fdehau/tui-rs/pull/300 to support wezterm backend. But not working as expected on windows.