Skip to content

[Feature] Bitmask-Based Component Event Filtering with Granular Handlers #91

@vmarcella

Description

@vmarcella

Overview

Refactor the Component trait's event handling system to replace the monolithic
on_event method with granular, opt-in event handlers and a bitmask-based
filtering system. This change eliminates boilerplate pattern matching in
components, improves runtime performance by skipping uninterested components,
and provides a more ergonomic API for component authors.

Current State

The current Component trait requires all components to implement on_event,
which receives every event and forces exhaustive pattern matching:

// crates/lambda-rs/src/component.rs:30-33
fn on_event(&mut self, event: Events) -> Result<R, E>;

Components must match against all event variants even when they only care about
a subset:

// Example from crates/lambda-rs/examples/textured_cube.rs:370-385
fn on_event(&mut self, event: Events) -> Result<ComponentResult, String> {
  match event {
    Events::Window { event, .. } => match event {
      WindowEvent::Resize { width, height } => {
        self.width = width;
        self.height = height;
      }
      _ => {}
    },
    _ => {}  // Must handle all other variants
  }
  return Ok(ComponentResult::Success);
}

This approach has several limitations:

  • Boilerplate: Every component must write _ => {} arms for unused events
  • Poor branch prediction: Runtime iterates all components for every event
  • No filtering: Components receive events they never handle

Scope

Goals:

  • Introduce EventMask bitmask type for O(1) event category filtering
  • Add granular on_*_event methods with default no-op implementations
  • Update ApplicationRuntime to skip components based on their event mask
  • Remove the on_event method from the Component trait
  • Update all examples to use the new event handling API

Non-Goals:

  • Generic Handles<E> trait system (adds complexity without runtime benefit
    when using trait objects)
  • Dynamic event subscription/unsubscription at runtime
  • Event prioritization or ordering guarantees beyond current behavior

Proposed API

EventMask (events.rs)

/// Bitmask for O(1) event category filtering.
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
pub struct EventMask(u8);

impl EventMask {
  pub const NONE: Self = Self(0);
  pub const WINDOW: Self = Self(1 << 0);
  pub const KEYBOARD: Self = Self(1 << 1);
  pub const MOUSE: Self = Self(1 << 2);
  pub const RUNTIME: Self = Self(1 << 3);
  pub const COMPONENT: Self = Self(1 << 4);

  /// Check if this mask contains the given event category.
  #[inline(always)]
  pub const fn contains(self, other: Self) -> bool {
    (self.0 & other.0) != 0
  }

  /// Combine two masks.
  #[inline(always)]
  pub const fn union(self, other: Self) -> Self {
    Self(self.0 | other.0)
  }
}

impl Events {
  /// Return the mask for this event's category.
  pub const fn mask(&self) -> EventMask {
    match self {
      Events::Window { .. } => EventMask::WINDOW,
      Events::Keyboard { .. } => EventMask::KEYBOARD,
      Events::Mouse { .. } => EventMask::MOUSE,
      Events::Runtime { .. } => EventMask::RUNTIME,
      Events::Component { .. } => EventMask::COMPONENT,
    }
  }
}

Updated Component Trait (component.rs)

pub trait Component<R, E>
where
  R: Sized + Debug,
  E: Sized + Debug,
{
  /// Return which event categories this component handles.
  /// The runtime skips dispatch for events not in this mask.
  fn event_mask(&self) -> EventMask {
    EventMask::NONE
  }

  /// Called when a window event occurs. Override to handle.
  fn on_window_event(&mut self, _event: &WindowEvent) {}

  /// Called when a keyboard event occurs. Override to handle.
  fn on_keyboard_event(&mut self, _event: &Key) {}

  /// Called when a mouse event occurs. Override to handle.
  fn on_mouse_event(&mut self, _event: &Mouse) {}

  /// Called when a runtime event occurs. Override to handle.
  fn on_runtime_event(&mut self, _event: &RuntimeEvent) {}

  /// Called when a component event occurs. Override to handle.
  fn on_component_event(&mut self, _event: &ComponentEvent) {}

  // Existing lifecycle methods remain unchanged
  fn on_attach(&mut self, render_context: &mut RenderContext) -> Result<R, E>;
  fn on_detach(&mut self, render_context: &mut RenderContext) -> Result<R, E>;
  fn on_update(&mut self, last_frame: &Duration) -> Result<R, E>;
  fn on_render(&mut self, render_context: &mut RenderContext) -> Vec<RenderCommand>;
}

Example Usage

impl Component<ComponentResult, String> for PlayerController {
  fn event_mask(&self) -> EventMask {
    EventMask::WINDOW
      .union(EventMask::KEYBOARD)
      .union(EventMask::MOUSE)
  }

  fn on_window_event(&mut self, event: &WindowEvent) {
    if let WindowEvent::Resize { width, height } = event {
      self.width = *width;
      self.height = *height;
    }
  }

  fn on_keyboard_event(&mut self, event: &Key) {
    if let Key::Pressed { virtual_key: Some(VirtualKey::Escape), .. } = event {
      self.paused = true;
    }
  }

  fn on_mouse_event(&mut self, event: &Mouse) {
    if let Mouse::Moved { x, y, .. } = event {
      self.cursor_position = (*x, *y);
    }
  }

  // on_runtime_event and on_component_event use default no-op

  fn on_attach(&mut self, _ctx: &mut RenderContext) -> Result<ComponentResult, String> {
    return Ok(ComponentResult::Success);
  }

  // ... other lifecycle methods
}

Acceptance Criteria

  • EventMask type added to events.rs with NONE, WINDOW, KEYBOARD,
    MOUSE, RUNTIME, COMPONENT constants
  • Events::mask() method returns the appropriate EventMask for each variant
  • Component trait updated with event_mask() and on_*_event() methods
  • on_event method removed from Component trait
  • ApplicationRuntime updated to filter components by mask before dispatch
  • All examples updated to use new event handling API
  • Unit tests added for EventMask operations
  • Documentation updated for Component trait and event handling

Affected Crates

lambda-rs

Notes

  • This change is breaking and requires updates to all existing components
  • The bitmask approach allows future extension (up to 8 event categories with
    u8, expandable to u16 or u32 if needed)

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions