From 8fb8c176e5ac2fe983721587d05db324eedf5b40 Mon Sep 17 00:00:00 2001 From: Adrian Papari Date: Sun, 8 Feb 2026 12:07:44 +0100 Subject: [PATCH 1/3] fix(dom): measure cell size on init --- Cargo.toml | 1 + src/backend/dom.rs | 70 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6da57910..a2bc5362 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ web-sys = { version = "0.3.81", features = [ 'WebGlTexture', 'WebGlUniformLocation', 'WebGlVertexArrayObject', + 'DomRect', 'Window', ] } compact_str = "0.9.0" diff --git a/src/backend/dom.rs b/src/backend/dom.rs index a2017c74..0176a85b 100644 --- a/src/backend/dom.rs +++ b/src/backend/dom.rs @@ -19,6 +19,9 @@ use unicode_width::UnicodeWidthStr; use crate::{backend::utils::*, error::Error, CursorShape}; +/// Default cell size used as a fallback when measurement fails. +const DEFAULT_CELL_SIZE: (f64, f64) = (10.0, 20.0); + /// Options for the [`DomBackend`]. #[derive(Debug, Default)] pub struct DomBackendOptions { @@ -83,6 +86,8 @@ pub struct DomBackend { last_cursor_position: Option, /// Buffer size to pass to [`ratatui::Terminal`] size: Size, + /// Measured cell dimensions in pixels (width, height). + cell_size: (f64, f64), } impl DomBackend { @@ -109,23 +114,75 @@ impl DomBackend { pub fn new_with_options(options: DomBackendOptions) -> Result { let window = window().ok_or(Error::UnableToRetrieveWindow)?; let document = window.document().ok_or(Error::UnableToRetrieveDocument)?; + let grid_parent = get_element_by_id_or_body(options.grid_id.as_ref())?; + let cell_size = Self::measure_cell_size(&document, &grid_parent) + .unwrap_or(DEFAULT_CELL_SIZE); + let size = Self::calculate_size(&grid_parent, cell_size); let mut backend = Self { initialized: Rc::new(RefCell::new(false)), cells: vec![], grid: document.create_element("div")?, - grid_parent: get_element_by_id_or_body(options.grid_id.as_ref())?, + grid_parent, options, window, document, cursor_position: None, last_cursor_position: None, - size: get_size(), + size, + cell_size, }; backend.add_on_resize_listener(); backend.reset_grid()?; Ok(backend) } + /// Measures the pixel dimensions of a single terminal cell. + /// + /// Creates a temporary `
` probe element that inherits the
+    /// page's CSS (font-family, font-size, etc.), measures it with
+    /// `getBoundingClientRect()`, then removes the probe.
+    fn measure_cell_size(document: &Document, parent: &Element) -> Result<(f64, f64), Error> {
+        let pre = document.create_element("pre")?;
+        pre.set_attribute("style", "margin: 0; padding: 0; border: 0; line-height: normal;")?;
+        let span = document.create_element("span")?;
+        span.set_inner_html("\u{2588}");
+        span.set_attribute("style", "display: inline-block; width: 1ch;")?;
+        pre.append_child(&span)?;
+        parent.append_child(&pre)?;
+
+        let rect = span.get_bounding_client_rect();
+        let width = rect.width();
+        let height = rect.height();
+
+        parent.remove_child(&pre)?;
+
+        if width > 0.0 && height > 0.0 {
+            Ok((width, height))
+        } else {
+            Ok(DEFAULT_CELL_SIZE)
+        }
+    }
+
+    /// Calculates the grid size in cells based on the parent element's dimensions and cell size.
+    fn calculate_size(parent: &Element, cell_size: (f64, f64)) -> Size {
+        let rect = parent.get_bounding_client_rect();
+        let (parent_w, parent_h) = (rect.width(), rect.height());
+
+        // Fall back to window dimensions if the parent has no size
+        // (e.g. empty  with no explicit height)
+        let (w, h) = if parent_w > 0.0 && parent_h > 0.0 {
+            (parent_w, parent_h)
+        } else {
+            let (ww, wh) = get_raw_window_size();
+            (ww as f64, wh as f64)
+        };
+
+        Size::new(
+            (w / cell_size.0) as u16,
+            (h / cell_size.1) as u16,
+        )
+    }
+
     /// Add a listener to the window resize event.
     fn add_on_resize_listener(&mut self) {
         let initialized = self.initialized.clone();
@@ -160,7 +217,8 @@ impl DomBackend {
 
             // Create a 
 element for the line
             let pre = self.document.create_element("pre")?;
-            pre.set_attribute("style", "height: 15px;")?;
+            let line_height = format!("height: {}px;", self.cell_size.1);
+            pre.set_attribute("style", &line_height)?;
 
             // Append all elements (spans and anchors) to the 
             for elem in line_cells {
@@ -201,8 +259,10 @@ impl Backend for DomBackend {
                 self.grid_parent.set_inner_html("");
                 self.reset_grid()?;
 
-                // update size
-                self.size = get_size();
+                // re-measure cell size and update grid dimensions
+                self.cell_size = Self::measure_cell_size(&self.document, &self.grid_parent)
+                    .unwrap_or(DEFAULT_CELL_SIZE);
+                self.size = Self::calculate_size(&self.grid_parent, self.cell_size);
             }
 
             self.grid_parent

From 74a1bb8b51b7ba4ae6ebbf530fbef678210d2722 Mon Sep 17 00:00:00 2001
From: Adrian Papari 
Date: Sun, 8 Feb 2026 12:17:58 +0100
Subject: [PATCH 2/3] formatting

---
 src/backend/dom.rs | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/src/backend/dom.rs b/src/backend/dom.rs
index 0176a85b..13892739 100644
--- a/src/backend/dom.rs
+++ b/src/backend/dom.rs
@@ -115,8 +115,8 @@ impl DomBackend {
         let window = window().ok_or(Error::UnableToRetrieveWindow)?;
         let document = window.document().ok_or(Error::UnableToRetrieveDocument)?;
         let grid_parent = get_element_by_id_or_body(options.grid_id.as_ref())?;
-        let cell_size = Self::measure_cell_size(&document, &grid_parent)
-            .unwrap_or(DEFAULT_CELL_SIZE);
+        let cell_size =
+            Self::measure_cell_size(&document, &grid_parent).unwrap_or(DEFAULT_CELL_SIZE);
         let size = Self::calculate_size(&grid_parent, cell_size);
         let mut backend = Self {
             initialized: Rc::new(RefCell::new(false)),
@@ -143,7 +143,10 @@ impl DomBackend {
     /// `getBoundingClientRect()`, then removes the probe.
     fn measure_cell_size(document: &Document, parent: &Element) -> Result<(f64, f64), Error> {
         let pre = document.create_element("pre")?;
-        pre.set_attribute("style", "margin: 0; padding: 0; border: 0; line-height: normal;")?;
+        pre.set_attribute(
+            "style",
+            "margin: 0; padding: 0; border: 0; line-height: normal;",
+        )?;
         let span = document.create_element("span")?;
         span.set_inner_html("\u{2588}");
         span.set_attribute("style", "display: inline-block; width: 1ch;")?;
@@ -177,10 +180,7 @@ impl DomBackend {
             (ww as f64, wh as f64)
         };
 
-        Size::new(
-            (w / cell_size.0) as u16,
-            (h / cell_size.1) as u16,
-        )
+        Size::new((w / cell_size.0) as u16, (h / cell_size.1) as u16)
     }
 
     /// Add a listener to the window resize event.

From 24db9582bd46b463098a8217cf45f968b1fbc9a6 Mon Sep 17 00:00:00 2001
From: Adrian Papari 
Date: Sun, 8 Feb 2026 12:30:26 +0100
Subject: [PATCH 3/3] implement DomBackend::window_size

---
 src/backend/dom.rs | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/backend/dom.rs b/src/backend/dom.rs
index 13892739..08d8f353 100644
--- a/src/backend/dom.rs
+++ b/src/backend/dom.rs
@@ -362,7 +362,13 @@ impl Backend for DomBackend {
     }
 
     fn window_size(&mut self) -> IoResult {
-        unimplemented!()
+        Ok(WindowSize {
+            columns_rows: self.size,
+            pixels: Size::new(
+                (self.size.width as f64 * self.cell_size.0) as u16,
+                (self.size.height as f64 * self.cell_size.1) as u16,
+            ),
+        })
     }
 
     fn get_cursor_position(&mut self) -> IoResult {