Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Event System

Cadmus uses a tree-based event system where views are organized hierarchically and events flow through two distinct channels: the Hub and the Bus. Understanding the difference between these channels is essential for implementing correct event handling in views.

Tip

CSS is hard, this page might render better on 80% zoom on smaller screens. I tried to make the mermaid diagrams as big as possible to make them easier to read, which made them overflow a bit.

You might also want to hide the sidebar.

Overview

The UI is a tree of View objects. Each view can have children, forming a hierarchy like:

flowchart TD
    Home["Home (root view)"]
    TopBar["TopBar"]
    Shelf["Shelf"]
    Book1["Book"]
    Book2["Book"]
    BottomBar["BottomBar"]
    Dialog["Dialog ← overlay child"]
    Label["Label"]
    Button["Button"]

    Home --> TopBar
    Home --> Shelf
    Home --> BottomBar
    Home --> Dialog
    Shelf --> Book1
    Shelf --> Book2
    Dialog --> Label
    Dialog --> Button

Events enter the tree from the main loop and travel top-down (root to leaves), with the highest z-level children checked first. Views can communicate back up the tree via the bus, or globally via the hub.

Hub vs Bus

The two channels serve fundamentally different purposes:

flowchart TB
    subgraph MainLoop["Main Loop"]
        rx["rx.recv()"]
        match["match evt { ... }"]
        handle["handle_event(root, &evt, &tx, &bus, ...)"]
        Hub["Hub (tx)"]
        Bus["Bus (VecDeque)"]

        rx --> match
        match -->|dispatches to view tree| handle
        handle --> Hub
        handle --> Bus
        Bus -->|unhandled bus events forwarded to hub| Hub
    end

Hub (Sender<Event>)

The hub is an mpsc::Sender<Event> — a global channel that sends events to the main loop. Events sent to the hub are processed in the next iteration of the main loop, not immediately.

Use the hub when:

  • The event needs to be handled by the main loop directly (e.g., Event::Close, Event::Open, Event::Notification)
  • The event should reach all views in a future dispatch cycle (e.g., Event::Focus)
  • You need to communicate across unrelated parts of the view tree
#![allow(unused)]
fn main() {
// Close a view — handled by the main loop's match statement
hub.send(Event::Close(self.view_id)).ok();

// Show a notification — main loop creates the Notification view
hub.send(Event::Notification(NotificationEvent::Show(msg))).ok();

// Set focus — dispatched to all views in the next loop iteration
hub.send(Event::Focus(Some(ViewId::SearchInput))).ok();
}

Bus (VecDeque<Event>)

The bus is a local VecDeque<Event> that passes events from a child to its parent. Events placed on the bus are handled synchronously during the current dispatch cycle.

Use the bus when:

  • A child needs to communicate with its direct parent
  • The parent is expected to handle the event (e.g., a button telling its parent dialog it was pressed)
  • The event should bubble up through the view hierarchy
#![allow(unused)]
fn main() {
// Child tells parent about a submission
bus.push_back(Event::Submit(self.view_id, self.text.clone()));

// Child requests parent to close it
bus.push_back(Event::Close(ViewId::MarginCropper));
}

Bus Bubbling

When a bus event is not handled by any ancestor view, it reaches the root and gets forwarded to the hub for processing in the next main loop iteration:

#![allow(unused)]
fn main() {
// End of main loop iteration — unhandled bus events become hub events
while let Some(ce) = bus.pop_front() {
    tx.send(ce).ok();
}
}

Event Dispatch

The core dispatch function in view/mod.rs controls how events flow through the tree:

flowchart TD
    Start["handle_event(view, event)"] --> CheckLeaf{"Is view a leaf<br/>(no children)?"}

    CheckLeaf -->|YES| HandleDirect["call view.handle_event()<br/>return result"]
    CheckLeaf -->|NO| ReverseIter["Step 1 - Iterate children in REVERSE order<br/>(highest z-level first)"]

    ReverseIter --> CheckChildren["For each child:<br/>call handle_event(child, event)"]
    CheckChildren --> Captured{"Child returns true?"}
    Captured -->|YES| SetCaptured["captured = true<br/>BREAK"]
    Captured -->|NO| NextChild["Next child"]
    NextChild --> CheckChildren

    SetCaptured --> ProcessBus["Step 2 - Process child_bus events"]
    CheckChildren -->|No more children| ProcessBus

    ProcessBus --> BusEvent{"For each event<br/>on child_bus"}
    BusEvent --> HandleBus["call view.handle_event(child_evt)"]
    HandleBus --> ViewHandles{"View handles it?"}
    ViewHandles -->|YES| RemoveBus["remove from bus"]
    ViewHandles -->|NO| KeepBus["keep on bus<br/>(bubbles up)"]
    RemoveBus --> BusEvent
    KeepBus --> BusEvent

    BusEvent -->|No more bus events| CheckCaptured{"NOT captured<br/>by any child?"}
    CheckCaptured -->|YES| HandleParent["Step 3 - call view.handle_event(event)<br/>return captured || view's result"]
    CheckCaptured -->|NO| ReturnCaptured["return captured"]

Key Rules

  1. Reverse iteration: Children are checked from last to first (highest z-level first). This ensures overlays like dialogs and menus receive events before the views beneath them.

  2. Short-circuit on capture: Once a child returns true, no other children or the parent view receive the event. This is why the Dialog’s outside-tap handler works — it returns true to prevent the event from reaching views behind it.

  3. Parent only runs if uncaptured: The parent’s handle_event is only called if no child captured the event (captured || view.handle_event(...)). If captured is true, the parent is short-circuited.

Main Loop Event Handling

The main loop (app.rs) receives events from the hub and handles them in a large match statement. Some events are dispatched into the view tree, while others are handled directly:

flowchart TB
    subgraph MainLoop["Main Loop"]
        direction TB

        Gesture["Event::Gesture(Tap/Swipe/...)"]
        GestureAction["Dispatched into view tree via handle_event()"]

        Close["Event::Close(id)"]
        CloseAction["Handled directly:<br/>locate_by_id() + remove"]

        Notification["Event::Notification(...)"]
        NotificationAction["Handled directly:<br/>create/update Notification"]

        Open["Event::Open(info)"]
        OpenAction["Handled directly:<br/>push Reader onto history"]

        Focus["Event::Focus(...)"]
        FocusAction["Dispatched into view tree via handle_event()"]

        Select["Event::Select(...)"]
        SelectAction["Some handled directly,<br/>some dispatched"]

        Gesture --> GestureAction
        Close --> CloseAction
        Notification --> NotificationAction
        Open --> OpenAction
        Focus --> FocusAction
        Select --> SelectAction
    end

Event::Close

Event::Close(ViewId) is handled directly by the main loop using locate_by_id(), which searches only the top-level children of the root view:

#![allow(unused)]
fn main() {
Event::Close(id) => {
    if let Some(index) = locate_by_id(view.as_ref(), id) {
        let rect = overlapping_rectangle(view.child(index));
        rq.add(RenderData::expose(rect, UpdateMode::Gui));
        view.children_mut().remove(index);
    }
}
}

Key limitation: The main loop can only remove direct children of the root. To close nested views, either:

  1. Use the parent’s ViewId (via hub): Removes the entire parent container
  2. Use the bus: Parent handles the close and removes just the specific child

See Why ViewId Matters for Close and Closing Nested Views via the Bus for details.

Practical Example: Dialog Outside-Tap

When a Dialog is tapped outside its bounds, the following sequence occurs:

sequenceDiagram
    actor User
    participant MainLoop as Main Loop
    participant ViewTree as View Tree
    participant Dialog as Dialog
    participant Parent as Parent View

    Note over User,Parent: 1. User taps at (x, y) outside the dialog

    User->>MainLoop: Tap gesture
    Note right of MainLoop: 2. Receives Event::Gesture(Tap(point))
    MainLoop->>ViewTree: Dispatches via handle_event()

    Note over ViewTree: 3. View tree dispatch (reverse child order)<br/>Dialog checked first (highest z-level)

    ViewTree->>Dialog: handle_event()
    Note right of Dialog: - Tap outside self.rect<br/>- Matches outside-tap arm<br/>- Sends Event::Close(self.view_id) to hub<br/>- Returns true (captured)
    Dialog-->>ViewTree: return true (captured)

    Note over Parent: 4. Parent SHORT-CIRCUITED<br/>(captured = true)<br/>Parent's handle_event() never called

    Note over MainLoop: 5. Next main loop iteration
    MainLoop->>MainLoop: Receives Event::Close(dialog_view_id)
    Note right of MainLoop: locate_by_id() finds dialog<br/>in top-level children<br/>Removes dialog from view tree

Why ViewId Matters for Close

When using the hub to close views, locate_by_id() only finds top-level children. If a dialog is nested inside a parent, you must use the parent’s ViewId — this removes the entire parent:

flowchart TD
    Root["Root View"]
    Home["Home"]
    OtaView["OtaView (top-level)"]
    Dialog["Dialog (nested)"]

    Root --> Home
    Root --> OtaView
    OtaView --> Dialog

    Note["hub.send(Event::Close(ViewId::Ota(Main)))<br/>→ Removes OtaView + Dialog"]

To close just the nested view without affecting siblings, use the bus instead (see below).

Closing Nested Views via the Bus

Send Event::Close via the bus to close nested views. Parents handle bus events synchronously and can remove any child directly:

#![allow(unused)]
fn main() {
// Child sends close via bus
bus.push_back(Event::Close(ViewId::Dialog));
}
sequenceDiagram
    participant Child as Button
    participant Dialog as Dialog (parent)
    participant Grandparent as OtaView

    Child->>Child: handle_event()
    Child-->>Dialog: bus: Event::Close(ViewId::Dialog)
    Dialog->>Dialog: Returns false (not handled)
    Dialog-->>Grandparent: Event bubbles up
    Grandparent->>Grandparent: Removes Dialog from children

Comparison:

MethodCodeHandlerScope
Hub closehub.send(Event::Close(id))Main loopTop-level children only
Bus closebus.push_back(Event::Close(id))Parent viewAny child

Example implementation:

#![allow(unused)]
fn main() {
impl View for Dialog {
    fn handle_event(&mut self, evt: &Event, hub: &Sender<Event>, bus: &mut Bus, ...) -> bool {
        match *evt {
            // Return false to bubble up so grandparent removes us
            Event::Close(ViewId::Dialog) => false,
            _ => false,
        }
    }
}
}

Summary

AspectHubBus
Typempsc::Sender<Event>VecDeque<Event>
ScopeGlobal (main loop)Local (parent-child)
TimingNext loop iterationCurrent dispatch cycle
DirectionView → Main loopChild → Parent
Unhandled eventsProcessed by main loop matchForwarded to hub
Use forClose, Focus, NotificationsSubmit, child-to-parent signals