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
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions