Skip to content

Conversation

@junkdog
Copy link
Collaborator

@junkdog junkdog commented Jun 30, 2025

Mouse events now report grid coordinates (col, row) instead of pixel coordinates, and event registration has been unified through the terminal API with proper lifecycle management.

Key Changes

  • Grid coordinates: MouseEvent now has col/row fields for terminal cell positions
  • Unified API: Event registration moved from backend options to terminal.on_mouse_event() / terminal.on_key_event()
  • Lifecycle management: Event callbacks are properly cleaned up and replaced to prevent memory leaks
  • Callback replacement: Registering a new callback automatically clears the previous one

Usage

// mut due to later holding onto the callbacks/closures
let mut terminal = MultiBackendBuilder::with_fallback(BackendType::Dom)
  .build_terminal()?;

terminal.on_mouse_event(|mouse_event| {
  println!("{:?} at cell: ({}, {})", mouse_event.kind, mouse_event.col, mouse_event.row);
}).ok(); // WebGL2 doesn't support mouse events yet

// This replaces any previous key event callback
terminal.on_key_event(|key_event| {
  println!("Key: {:?}", key_event.code);
}).ok();

Mouse Event Types

Available MouseEventKind variants:

  • Moved - mouse move event
  • ButtonDown(MouseButton) - button pressed
  • ButtonUp(MouseButton) - button released
  • Entered - mouse entered terminal area
  • Exited - mouse left terminal area
  • SingleClick(MouseButton) - single click (down + up)
  • DoubleClick(MouseButton), - single click x2 in quick succession
  • Wheel { delta_x, delta_y, delta_z } - scroll in delta cols, rows (should remove z)

Breaking Changes

  • Mouse coordinates changed from pixels to grid cells
  • Event registration: backend_options.mouse_event_handler() → terminal.on_mouse_event()
  • Terminal must be mut for event registration
  • Both mouse and key callbacks are now lifecycle managed (prevents leaks, replaces previous callbacks)

Backend Support

Copy link
Owner

@orhun orhun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, pretty clean and works!

Had some comments

Comment on lines 222 to 253
let width = if width > 0 { width } else { 10 };
let height = if height > 0 { height } else { 19 };
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We defined these constants somewhere, probably those can be used.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added MouseEventHandler - a simple wrapper around the boxed fn.

///
/// This function creates a span with the same styling as terminal cells,
/// measures its dimensions, and then removes it from the DOM.
pub(super) fn measure_dom_cell_size(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious about a couple of things:

  • Is this being affected by the font being used by the app? (e.g. defined in index.html)
  • We should probably avoid calling this frequently, because manipulating DOM takes time. Is that right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'll have to confirm - but iirc it's sparsely called (at init/resize maybe).

We should probably avoid calling this frequently, because manipulating DOM takes time. Is that right?

this is at least 1 or 2 PR:s ;) the DomBackend frequently does a layout pass on everything, at least that what i gathered from a brief look at the profiler. there's also a lack of memoization around all the CSS style strings, but in the grand scheme of things - it's not the current bottleneck (it marginally improved perf with the current impl).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only called during init; i wonder if it handles browser zoom levels correctly. (i'll check tomorrow)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i wonder if it handles browser zoom levels correctly.

it does

Comment on lines 193 to 204
if let Ok(element) = target.dyn_into::<web_sys::Element>() {
let rect = element.get_bounding_client_rect();
let grid_rect = (rect.left(), rect.top(), rect.width(), rect.height());
handler(MouseEvent::new_relative(event, cell_size_px, grid_rect));
} else {
// Fallback to viewport-relative coordinates if we can't get the element
handler(MouseEvent::new(event, cell_size_px));
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what's going on here. What's new vs new_relative?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, i don't recall exactly - if it's hypothetical or encountered, but the new_relative is the mouse coordinates relative to the canvas (and not the window)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so, during DOM rebuild, i think mouse events can still fire. i'm sure i can't replicate the behavior though - should we skip the fallback and only process the mouse event when we're sure that the cursor is hovering a cell. yeah... makes sense when i write it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replaced it with debug_assert() - better no event than a wrong event

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

::new_relative has been removed in the latest series of commits

Comment on lines 43 to 67
/// Handles mouse events.
///
/// This method takes a closure that will be called on every `mousemove`, 'mousedown', and `mouseup`
/// event.
fn on_mouse_event<F>(&self, mut callback: F)
where
F: FnMut(MouseEvent) + 'static,
{
let closure = Closure::<dyn FnMut(_)>::new(move |event: web_sys::MouseEvent| {
callback(event.into());
});
let window = window().unwrap();
let document = window.document().unwrap();
document
.add_event_listener_with_callback("mousemove", closure.as_ref().unchecked_ref())
.unwrap();
document
.add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref())
.unwrap();
document
.add_event_listener_with_callback("mouseup", closure.as_ref().unchecked_ref())
.unwrap();
closure.forget();
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense to do this in the Backend options, but I think it causes an inconsistency. We should follow the same style for all the events and callbacks imo.

Copy link
Collaborator Author

@junkdog junkdog Jul 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you suggesting we move it into Terminal (or the other way round)?

For it to work as expected, we'd need some sort of lifecycle management on the closure (and not closure.forget()), so that closures can be added and removed. adding a second event callback is UB territory without deregistering the old callback. I assume this applies to the current key event callback registration too, but it didn't occur to me until now.

So, either we move it back to Terminal and fix the issues with double callback registration or we move the key event registration to backend init. For the former to work, I guess we need to figure out the flow too (e.g., should a new callback replace the old one or do we want to support multiple callbacks per type).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(let me confirm this is actually the issue and not a misinterpretation of previous results)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated description, but it's been moved back into WebRenderer/WebEventHandler.

@junkdog junkdog force-pushed the mouse-grid-coords branch from 0541f05 to 28c09f8 Compare July 21, 2025 18:20
@junkdog
Copy link
Collaborator Author

junkdog commented Jul 21, 2025

current issue: dom backend pos are offset, canvas coordinates are likely inverted in relation to browser zoom level

@junkdog
Copy link
Collaborator Author

junkdog commented Jul 23, 2025

updated PR description to be more comprehensive

@junkdog
Copy link
Collaborator Author

junkdog commented Jul 23, 2025

i'll take another look through the code tomorrow, but i think this addresses all mentioned issues + fixes stuff i didn't think about previously (support mouse wheel events, exposing all mouse events leaving the choice of low-level or high-level mouse events to the user, offset bug fix etc),

the minimal example has been updated to show the current cursor position and event data. maybe we should copy it to minimal_mouse_input and restore the old minimal example?

@junkdog junkdog force-pushed the mouse-grid-coords branch from 0a9ee70 to 8c2f781 Compare July 24, 2025 17:29
@junkdog
Copy link
Collaborator Author

junkdog commented Jul 24, 2025

i guess that's it; callback lifecycle management it a bit more managed. i've also restored the old "minimal" example and copied the other minimal to a new example: "mouse".

@0x01d
Copy link

0x01d commented Jul 25, 2025

Thx dog, this is exactly what I needed, right when I needed it. Going to give it a whirl :b

@orhun
Copy link
Owner

orhun commented Aug 2, 2025

@0x01d have you tried this yet?

Man... I need to re-review this sometime

@0x01d
Copy link

0x01d commented Aug 9, 2025

@orhun Yes it worked like a charm. I use it on my website rbn.dev.

edit: All is fine error on my end..

@orhun
Copy link
Owner

orhun commented Aug 9, 2025

Damn, your website looks pretty cool! I should also rebuild my blog using ratzilla sometime.

Do you mind adding it to the docs here? 😇

@0x01d
Copy link

0x01d commented Aug 12, 2025

@orhun Would love that, thx.The whole codebase is open source on my profile. Would love some feedback, not immediately though as it still needs a massive refactor -mvp state right now.

@orhun
Copy link
Owner

orhun commented Sep 21, 2025

Added in #126 :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants