-
Notifications
You must be signed in to change notification settings - Fork 61
Allow canvas backend to resize dynamically #162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3d977ec
e5226a7
be2402d
739cce7
f1f6687
50b5ef7
b631a38
192ee67
e4841c7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,10 @@ | ||||||||
| use bitvec::{bitvec, prelude::BitVec}; | ||||||||
| use ratatui::{backend::ClearType, layout::Rect}; | ||||||||
| use std::io::{Error as IoError, Result as IoResult}; | ||||||||
| use std::{ | ||||||||
| io::{Error as IoError, Result as IoResult}, | ||||||||
| rc::Rc, | ||||||||
| sync::atomic::{AtomicBool, AtomicU16, Ordering}, | ||||||||
| }; | ||||||||
|
|
||||||||
| use crate::{ | ||||||||
| backend::{ | ||||||||
|
|
@@ -77,21 +81,46 @@ impl CanvasBackendOptions { | |||||||
| struct Canvas { | ||||||||
| /// Canvas element. | ||||||||
| inner: web_sys::HtmlCanvasElement, | ||||||||
| /// Canvas parent element. | ||||||||
| parent: Option<web_sys::Element>, | ||||||||
| /// Rendering context. | ||||||||
| context: web_sys::CanvasRenderingContext2d, | ||||||||
| /// Background color. | ||||||||
| background_color: Color, | ||||||||
| /// If the canvas is a fixed size, that size | ||||||||
| size: Option<(u32, u32)>, | ||||||||
| } | ||||||||
|
|
||||||||
| impl Canvas { | ||||||||
| /// Constructs a new [`Canvas`]. | ||||||||
| fn new( | ||||||||
| parent_element: web_sys::Element, | ||||||||
| width: u32, | ||||||||
| height: u32, | ||||||||
| parent_element: Option<web_sys::Element>, | ||||||||
| size: Option<(u32, u32)>, | ||||||||
| background_color: Color, | ||||||||
| ) -> Result<Self, Error> { | ||||||||
| let canvas = create_canvas_in_element(&parent_element, width, height)?; | ||||||||
| let (width, height) = size | ||||||||
| .or_else(|| { | ||||||||
| parent_element | ||||||||
| .as_ref() | ||||||||
| .map(|p| (p.client_width() as u32, p.client_height() as u32)) | ||||||||
| }) | ||||||||
| .unwrap_or_else(|| { | ||||||||
| let (width, height) = get_raw_window_size(); | ||||||||
| (width as u32, height as u32) | ||||||||
| }); | ||||||||
|
|
||||||||
| let canvas = if let Some(element) = parent_element.as_ref() { | ||||||||
| create_canvas_in_element(element, width, height)? | ||||||||
| } else { | ||||||||
| create_canvas_in_element( | ||||||||
| &get_document()? | ||||||||
| .body() | ||||||||
| .ok_or(Error::UnableToRetrieveBody)? | ||||||||
| .into(), | ||||||||
| width, | ||||||||
| height, | ||||||||
| )? | ||||||||
| }; | ||||||||
|
|
||||||||
| let context_options = Map::new(); | ||||||||
| context_options.set(&JsValue::from_str("alpha"), &Boolean::from(JsValue::TRUE)); | ||||||||
|
|
@@ -104,15 +133,60 @@ impl Canvas { | |||||||
| .ok_or_else(|| Error::UnableToRetrieveCanvasContext)? | ||||||||
| .dyn_into::<web_sys::CanvasRenderingContext2d>() | ||||||||
| .expect("Unable to cast canvas context"); | ||||||||
| context.set_font("16px monospace"); | ||||||||
| context.set_text_baseline("top"); | ||||||||
|
|
||||||||
| Ok(Self { | ||||||||
| inner: canvas, | ||||||||
| parent: parent_element, | ||||||||
| context, | ||||||||
| background_color, | ||||||||
| size, | ||||||||
| }) | ||||||||
| } | ||||||||
|
|
||||||||
| /// Returns true if the size changed | ||||||||
| fn init_ctx_and_resize(&self) -> Result<Option<(usize, usize)>, Error> { | ||||||||
| let (width, height) = self | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this snippet is duplicated in new(), should prob go into its own function. would also improve readability. |
||||||||
| .size | ||||||||
| .or_else(|| { | ||||||||
| self.parent | ||||||||
| .as_ref() | ||||||||
| .map(|p| (p.client_width() as u32, p.client_height() as u32)) | ||||||||
| }) | ||||||||
| .unwrap_or_else(|| { | ||||||||
| let (width, height) = get_raw_window_size(); | ||||||||
| (width as u32, height as u32) | ||||||||
| }); | ||||||||
|
|
||||||||
| let ratio = web_sys::window() | ||||||||
| .ok_or(Error::UnableToRetrieveWindow)? | ||||||||
|
Comment on lines
+160
to
+161
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| .device_pixel_ratio(); | ||||||||
|
|
||||||||
| let source_w = (width as f64 / CELL_WIDTH).floor(); | ||||||||
| let source_h = (height as f64 / CELL_HEIGHT).floor(); | ||||||||
|
|
||||||||
| let canvas_w = source_w * CELL_WIDTH; | ||||||||
| let canvas_h = source_h * CELL_HEIGHT; | ||||||||
|
|
||||||||
| let change_size = self.inner.width() != (canvas_w * ratio) as u32 | ||||||||
| || self.inner.height() != (canvas_h * ratio) as u32; | ||||||||
|
|
||||||||
| if change_size { | ||||||||
| self.inner.set_width((canvas_w * ratio) as u32); | ||||||||
| self.inner.set_height((canvas_h * ratio) as u32); | ||||||||
| self.inner | ||||||||
| .style() | ||||||||
| .set_property("width", &format!("{}px", canvas_w))?; | ||||||||
| self.inner | ||||||||
| .style() | ||||||||
| .set_property("height", &format!("{}px", canvas_h))?; | ||||||||
|
|
||||||||
| self.context.set_font("16px monospace"); | ||||||||
| self.context.set_text_baseline("top"); | ||||||||
| self.context.scale(ratio, ratio)?; | ||||||||
| } | ||||||||
|
|
||||||||
| Ok(change_size.then_some((source_w as usize, source_h as usize))) | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| /// Canvas backend. | ||||||||
|
|
@@ -121,12 +195,14 @@ impl Canvas { | |||||||
| #[derive(Debug)] | ||||||||
| pub struct CanvasBackend { | ||||||||
| /// Whether the canvas has been initialized. | ||||||||
| initialized: bool, | ||||||||
| initialized: Rc<AtomicBool>, | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||
| /// Always clip foreground drawing to the cell rectangle. Helpful when | ||||||||
| /// dealing with out-of-bounds rendering from problematic fonts. Enabling | ||||||||
| /// this option may cause some performance issues when dealing with large | ||||||||
| /// numbers of simultaneous changes. | ||||||||
| always_clip_cells: bool, | ||||||||
| /// The size of the buffer in cells | ||||||||
| grid_size: Rc<(AtomicU16, AtomicU16)>, | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here, |
||||||||
| /// Current buffer. | ||||||||
| buffer: Vec<Vec<Cell>>, | ||||||||
| /// Previous buffer. | ||||||||
|
|
@@ -145,6 +221,8 @@ pub struct CanvasBackend { | |||||||
| mouse_callback: Option<MouseCallbackState>, | ||||||||
| /// Key event callback handler. | ||||||||
| key_callback: Option<EventCallback<web_sys::KeyboardEvent>>, | ||||||||
| /// Resize event callback handler | ||||||||
| _resize_callback: EventCallback<web_sys::Event>, | ||||||||
| } | ||||||||
|
|
||||||||
| /// Type alias for mouse event callback state. | ||||||||
|
|
@@ -168,27 +246,47 @@ impl CanvasBackend { | |||||||
| /// Constructs a new [`CanvasBackend`] with the given options. | ||||||||
| pub fn new_with_options(options: CanvasBackendOptions) -> Result<Self, Error> { | ||||||||
| // Parent element of canvas (uses <body> unless specified) | ||||||||
| let parent = get_element_by_id_or_body(options.grid_id.as_ref())?; | ||||||||
| let parent = if let Some(id) = options.grid_id.as_ref() { | ||||||||
| Some(get_element_by_id_or_body(Some(id))?) | ||||||||
| } else { | ||||||||
| None | ||||||||
| }; | ||||||||
|
|
||||||||
| let (width, height) = options | ||||||||
| .size | ||||||||
| .unwrap_or_else(|| (parent.client_width() as u32, parent.client_height() as u32)); | ||||||||
| let canvas = Canvas::new(parent, options.size, Color::Black)?; | ||||||||
|
|
||||||||
| let initialized = Rc::new(AtomicBool::new(false)); | ||||||||
|
|
||||||||
| let resize_callback = EventCallback::new( | ||||||||
| web_sys::window().ok_or(Error::UnableToRetrieveWindow)?, | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
from backend/utils.rs |
||||||||
| &["resize"], | ||||||||
| { | ||||||||
| let initialized = Rc::clone(&initialized); | ||||||||
| move |_: web_sys::Event| { | ||||||||
| initialized.store(false, Ordering::Relaxed); | ||||||||
| } | ||||||||
| }, | ||||||||
| )?; | ||||||||
|
|
||||||||
| let canvas = Canvas::new(parent, width, height, Color::Black)?; | ||||||||
| let buffer = get_sized_buffer_from_canvas(&canvas.inner); | ||||||||
| let changed_cells = bitvec![0; buffer.len() * buffer[0].len()]; | ||||||||
| let buffer_size = Rc::new(( | ||||||||
| AtomicU16::new(buffer[0].len() as u16), | ||||||||
| AtomicU16::new(buffer.len() as u16), | ||||||||
| )); | ||||||||
| Ok(Self { | ||||||||
| prev_buffer: buffer.clone(), | ||||||||
| always_clip_cells: options.always_clip_cells, | ||||||||
| buffer, | ||||||||
| initialized: false, | ||||||||
| grid_size: buffer_size, | ||||||||
| initialized, | ||||||||
| changed_cells, | ||||||||
| canvas, | ||||||||
| cursor_position: None, | ||||||||
| cursor_shape: CursorShape::SteadyBlock, | ||||||||
| debug_mode: None, | ||||||||
| mouse_callback: None, | ||||||||
| key_callback: None, | ||||||||
| _resize_callback: resize_callback, | ||||||||
| }) | ||||||||
| } | ||||||||
|
|
||||||||
|
|
@@ -241,7 +339,6 @@ impl CanvasBackend { | |||||||
| self.canvas.inner.client_height() as f64, | ||||||||
| ); | ||||||||
| } | ||||||||
| self.canvas.context.translate(5_f64, 5_f64)?; | ||||||||
|
|
||||||||
| // NOTE: The draw_* functions each traverse the buffer once, instead of | ||||||||
| // traversing it once per cell; this is done to reduce the number of | ||||||||
|
|
@@ -254,7 +351,6 @@ impl CanvasBackend { | |||||||
| self.draw_debug()?; | ||||||||
| } | ||||||||
|
|
||||||||
| self.canvas.context.translate(-5_f64, -5_f64)?; | ||||||||
| Ok(()) | ||||||||
| } | ||||||||
|
|
||||||||
|
|
@@ -474,10 +570,31 @@ impl Backend for CanvasBackend { | |||||||
| /// actually render the content to the screen. | ||||||||
| fn flush(&mut self) -> IoResult<()> { | ||||||||
| // Only runs once. | ||||||||
| if !self.initialized { | ||||||||
| if !self.initialized.swap(true, Ordering::Relaxed) { | ||||||||
| if let Some(new_buffer_size) = self.canvas.init_ctx_and_resize()? { | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||
| self.buffer.resize_with(new_buffer_size.1, || { | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm, shouldn't the resize check run in draw()? as in, ratatui has already resized the buffer in draw(), so any cells in the diff will be lost since they fall outside the internal cell |
||||||||
| vec![Cell::default(); new_buffer_size.0] | ||||||||
| }); | ||||||||
| self.prev_buffer.resize_with(new_buffer_size.1, || { | ||||||||
| vec![Cell::default(); new_buffer_size.0] | ||||||||
| }); | ||||||||
| for line in self.buffer.iter_mut() { | ||||||||
| line.resize_with(new_buffer_size.0, || Cell::default()); | ||||||||
| } | ||||||||
| for line in self.prev_buffer.iter_mut() { | ||||||||
| line.resize_with(new_buffer_size.0, || Cell::default()); | ||||||||
| } | ||||||||
| self.changed_cells | ||||||||
| .resize(new_buffer_size.0 * new_buffer_size.1, true); | ||||||||
| self.grid_size | ||||||||
| .0 | ||||||||
| .store(new_buffer_size.0 as u16, Ordering::Relaxed); | ||||||||
| self.grid_size | ||||||||
| .1 | ||||||||
| .store(new_buffer_size.1 as u16, Ordering::Relaxed); | ||||||||
| } | ||||||||
| self.update_grid(true)?; | ||||||||
| self.prev_buffer = self.buffer.clone(); | ||||||||
| self.initialized = true; | ||||||||
| return Ok(()); | ||||||||
| } | ||||||||
|
|
||||||||
|
|
@@ -517,14 +634,17 @@ impl Backend for CanvasBackend { | |||||||
| } | ||||||||
|
|
||||||||
| fn clear(&mut self) -> IoResult<()> { | ||||||||
| self.buffer = get_sized_buffer(); | ||||||||
| self.buffer | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice |
||||||||
| .iter_mut() | ||||||||
| .flatten() | ||||||||
| .for_each(|c| *c = Cell::default()); | ||||||||
| Ok(()) | ||||||||
| } | ||||||||
|
|
||||||||
| fn size(&self) -> IoResult<Size> { | ||||||||
| Ok(Size::new( | ||||||||
| self.buffer[0].len().saturating_sub(1) as u16, | ||||||||
| self.buffer.len().saturating_sub(1) as u16, | ||||||||
| self.grid_size.0.load(Ordering::Relaxed), | ||||||||
| self.grid_size.1.load(Ordering::Relaxed), | ||||||||
| )) | ||||||||
| } | ||||||||
|
|
||||||||
|
|
@@ -570,27 +690,22 @@ impl WebEventHandler for CanvasBackend { | |||||||
| // Clear any existing handlers first | ||||||||
| self.clear_mouse_events(); | ||||||||
|
|
||||||||
| // Get grid dimensions from the buffer | ||||||||
| let grid_width = self.buffer[0].len() as u16; | ||||||||
| let grid_height = self.buffer.len() as u16; | ||||||||
|
|
||||||||
| // Configure coordinate translation for canvas backend | ||||||||
| let config = MouseConfig::new(grid_width, grid_height) | ||||||||
| .with_offset(5.0) // Canvas translation offset | ||||||||
| .with_cell_dimensions(CELL_WIDTH, CELL_HEIGHT); | ||||||||
|
|
||||||||
| let element: web_sys::Element = self.canvas.inner.clone().into(); | ||||||||
| let element_for_closure = element.clone(); | ||||||||
|
|
||||||||
| // Create mouse event callback | ||||||||
| let mouse_callback = EventCallback::new( | ||||||||
| element, | ||||||||
| MOUSE_EVENT_TYPES, | ||||||||
| let mouse_callback = EventCallback::new(element, MOUSE_EVENT_TYPES, { | ||||||||
| let grid_size = Rc::clone(&self.grid_size); | ||||||||
| move |event: web_sys::MouseEvent| { | ||||||||
| let config = MouseConfig::new( | ||||||||
| grid_size.0.load(Ordering::Relaxed), | ||||||||
| grid_size.1.load(Ordering::Relaxed), | ||||||||
| ) | ||||||||
| .with_cell_dimensions(CELL_WIDTH, CELL_HEIGHT); | ||||||||
| let mouse_event = create_mouse_event(&event, &element_for_closure, &config); | ||||||||
| callback(mouse_event); | ||||||||
| }, | ||||||||
| )?; | ||||||||
| } | ||||||||
| })?; | ||||||||
|
|
||||||||
| self.mouse_callback = Some(mouse_callback); | ||||||||
|
|
||||||||
|
|
||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this function would benefit from being multiple functions. it's does a few too many things atm.