Skip to content

Instantly share code, notes, and snippets.

@giannissc
Last active January 12, 2024 18:46
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save giannissc/2aabe0449d3e1b6c961bfa68c3f1e1c2 to your computer and use it in GitHub Desktop.
Save giannissc/2aabe0449d3e1b6c961bfa68c3f1e1c2 to your computer and use it in GitHub Desktop.

General

  • Event and Lifecycle are propagated down the entire tree but if every single events was processed by every widget that would cause performance issues.
  • The mechanism which decides which events and lifecycles will be propagated to which parts of the tree is the Pod
  • Some events are global (e.g. WindowSize) and are handled directly by the Pod and some are local and targeted to specific subset of widgets in the tree (e.g.MouseMove).
  • Targeted events are controlled by the IS_DISABLED, IS_HANDLED, HAS_ACTIVE, IS_HOT, and HAS_FOCUS flags.

Event Handling Infrastructure

  • The IS_HANDLED flag is set to false at the root of the tree by the Pod
  • The widget that processes the event must set the IS_HANDLED flag through its context.
  • The IS_HANDLED flag applies to keyboard and mouse events.
  • Each level in the tree checks the IS_HANDLED flag to decide propagation. The check happens in the Pod

Window Event Infrastructure

  • The fundamental window events are WindowConnected, WindowCloseRequested, and WindowDisconnected and are always sent to all widgets in the tree.
  • The WindowConfig controls the WindowSize event.
  • The WindowScale is sent every time the OS changes the resolution of the screen or scale of the screen.
  • The WindowSize and WindowScale are handled by the Pod and trigger a relayout.
  • The WindowConnected, WindowCloseRequested, WindowDisconnected, WindowScale, and WindowSize are the associated events to the window event infrastructure.
  • There are no associated lifecycles to the window event infrastructure.
pub struct WindowConfig {
    pub(crate) size_policy: WindowSizePolicy,
    pub(crate) size: Option<Size>,
    pub(crate) min_size: Option<Size>,
    pub(crate) position: Option<Point>,
    pub(crate) resizable: Option<bool>,
    pub(crate) transparent: Option<bool>,
    pub(crate) show_titlebar: Option<bool>,
    pub(crate) level: Option<WindowLevel>,
    pub(crate) always_on_top: Option<bool>,
    pub(crate) state: Option<WindowState>,
}

Mouse Event Infrastructure

  • The Pod is responsible for controlling the propagation of mouse event using the IS_HOT and HAS_ACTIVE flags.
  • The IS_HOT is an implicit control mechanism and the HAS_ACTIVE an explicit contorl mechanism for mouse events.
  • The Pod sets the IS_HOT flag for Widgets whose area intersects with the pointer. A widget will always receive mouse events when the IS_HOT flag is active.
  • If a widget wishes to receive event after the pointer has left its area the widget must set its IS_ACTIVE flags. When it no longer wishes to receive the mouse events it must reset it.
  • The HAS_ACTIVE flags is automatically set by the Pod for all the ancestors of any widget with an IS_ACTIVE flag.
  • The MouseDown(MouseEvent), MouseUp(MouseEvent), MouseMove(MouseEvent), MouseWheel(MouseEvent), MouseLeft() are the associated events to the mouse event infrastructure
  • The HotChanged is the associated lifecycle to the mouse event infrastructure.
pub struct MouseEvent {
    pub pos: Point,
    pub window_pos: Point,
    pub buttons: MouseButtons,
    pub mods: Modifiers,
    pub count: u8,
    pub focus: bool,
    pub button: MouseButton,
    pub wheel_delta: Vec2,
}
pub(crate) enum CursorChange {
    /// No cursor has been set.
    Default,
    /// Someone set a cursor, but if a child widget also set their cursor then we'll use theirs
    /// instead of ours.
    Set(Cursor),
    /// Someone set a cursor, and we'll use it regardless of what the children say.
    Override(Cursor),
}

Keyboard and IME Event Infrastructure

  • The keyboard can only be used by a single widget at a time.
  • Widgets can receive keyboard event implicitly by adding themselves to the focus chain using the register_for_focus() or explicitly by sending a request_focus() through their context. Explicit request take presedence over implicit requests. The last widget to request focus gets it. The App manages the focused widget.
  • Widgets must use register_text_input() when added to the tree.
  • A widget can check if it has keyboard focus using is_focused() or if any of it's descendant have focus using has_focus()
  • The HAS_FOCUS flag is automatically set by the Pod for all the ancestors of the widget with the active focus.
  • A widget can also transfer its keyboard focus to a specific widget using set_focus(IdPath), to the previous or next widget in the focus chain using focus_prev() or focus_next() respectively. A widget can also simply resign focus using resign_focus().
  • When the focus changes a LifeCycle::FocusChanged is sent.
  • The LifeCycle::RebuildFocusChain is sent whenever the tree is updated. The register_for_focus() is called when handling the lifecycle.
  • The KeyDown(KeyEvent), and KeyUp(KeyEvent) are the associated events to the keyboard event infrastructure.
  • The BuildFocusChain, and FocusChanged(bool) are the associated lifecycles to the keyboard event infrastructure.
  • Druid IME
  • Glazier edit_text example
pub struct KeyEvent {
    pub state: KeyState,
    pub key: KbKey,
    pub code: Code,
    pub location: Location,
    pub mods: Modifiers,
    pub repeat: bool,
    pub is_composing: bool,
}
pub(crate) enum FocusChange {
    /// The focused widget is giving up focus.
    Resign,
    /// A specific widget wants focus
    Focus(WidgetId),
    /// Focus should pass to the next focusable widget
    Next,
    /// Focus should pass to the previous focusable widget
    Previous,
}

Accessibility Infrastructure

Animated Frame Event Infrastructure

  • A widget requests an animated frame through its context.
  • The frame is routed to glazier and at a later time (implicit control) an AnimFrame is sent with the time ellapsed since the request was made. The widgets that wish to animate themselves can modify their internal state using that value.
  • If an an element transition is not finished widgets can keep requesting animated frames until they are done.
  • Widgets must remember to make a paint request for the changes to be shown on screen. They must also not request more animated events than necessary cause that can cause performance issues.
  • The AnimFrame(u64) is the associated event to the animated frame event infrastructure.

Timer Event Infrastructure

  • A widget requests a timer through its context.
  • The frame is routed to glazier and a TimerToken is returned. The framework track which IdPath is associated to which TimerToken
  • At a later time (explicit control) a TimerToken is sent and routed to the widget.
  • The Timer(TimerToken) is the associated event to the timer event infrastructure.

Disabled Widget Event Infrastructure

  • A widget is disabled if itself or an ancestor is
  • The disabled status is controlled by the widget itself and set through its context
  • A widget must adapt its event handling and rendering in response to its disabled status
  • The user can control the disabled status of of widgets by wrapping widgets with DisabledView and passing a callback with the disable condition.
  • When a widget's disabled status is changed a LifeCycle::DisabledChanged is sent
  • What's the purpose of children_disabled?
  • The DisabledChanged(bool) is the associated lifecycle to the disabled event infrastructure

View Context Changed Set Origin Infrstructure

  • A layout stage consists of a sizing and a positioning pass.
  • The size is calculated during and the positioning after layout().
  • If the size of an element changes between cycles a LifeCycle::Size(Size) is sent. The child will receive the LifeCycle::Size(Size) event informing them of the final Size.
  • A request to recalculate positioning is made during layout() by setting the NEEDS_SET_ORIGIN flag. The origin of a widget is calcualted in the paren't coordinate space and performed after layout() has finished
  • If the origin has indeed changed the VIEW_CONTEXT_CHANGED and CHILDREN_VIEW_CONTEXT_CHANGED flags are set and a Lifecycle::ViewContextChanged is sent. The hot state must be recalculated when processing the Lifecycle::ViewContextChanged.
  • The ViewContextChanged and Size(Size) are the associated lifecycles to view context infrastructure
pub struct ViewContext {
    pub window_origin: Point,
    pub clip: Rect,
    pub last_mouse_position: Option<Point>,
}

Tree Updates Infrastructure

  • Relevant for ViewSequence.
  • A ChangeFlags::TREE is returned to the framework after rebuild() and the PodFlags::TREE_CHANGED flag is set for the root of the tree
  • A LifeCycle::TreeUpdate is sent and the tree is rebuilt.

Xilem Types

ChangeFlags and PodFlags structs

  • The PodFlags are local to widgets
  • The ChangeFlags are returned to the framework and applied to the root of the tree.
pub struct ChangeFlags: u8 {
    const UPDATE = 1;
    const LAYOUT = 2;
    const ACCESSIBILITY = 4;
    const PAINT = 8;
    const TREE = 0x10;
    const DESCENDANT_REQUESTED_ACCESSIBILITY = 0x20;
    const ANIM = 0x40;
    const DISABLED = 0x80;    
}
pub(crate) struct PodFlags: u32 {
    const REQUEST_UPDATE = ChangeFlags::UPDATE.bits() as _;
    const REQUEST_LAYOUT = ChangeFlags::LAYOUT.bits() as _;
    const REQUEST_ACCESSIBILITY = ChangeFlags::ACCESSIBILITY.bits() as _;
    const REQUEST_PAINT = ChangeFlags::PAINT.bits() as _;
    const REQUEST_ANIM = ChangeFlags::ANIM.bits() as _;
    const TREE_CHANGED = ChangeFlags::TREE.bits() as _;
    const DESCENDANT_REQUESTED_ACCESSIBILITY = ChangeFlags::DESCENDANT_REQUESTED_ACCESSIBILITY.bits() as _;

    // Everything else uses bitmasks greater than the max value of ChangeFlags: mask >= 0x100
    const VIEW_CONTEXT_CHANGED = 0x100;

    const IS_HOT = 0x200;
    const IS_ACTIVE = 0x400;
    const HAS_ACTIVE = 0x800;

    const NEEDS_SET_ORIGIN = 0x1000;
    
    const HAS_FOCUS = 0x2000;
    const UPDATE_FOCUS = 0x4000;
    
    const IS_DISABLED = 0x80000;
    const HAS_DISABLED = 0x10000;

    const UPWARD_FLAGS = Self::REQUEST_UPDATE.bits()
        | Self::REQUEST_LAYOUT.bits()
        | Self::REQUEST_PAINT.bits()
        | Self::HAS_ACTIVE.bits()
        | Self::DESCENDANT_REQUESTED_ACCESSIBILITY.bits()
        | Self::TREE_CHANGED.bits()
        | Self::VIEW_CONTEXT_CHANGED.bits();
    const INIT_FLAGS = Self::REQUEST_UPDATE.bits()
        | Self::REQUEST_LAYOUT.bits()
        | Self::REQUEST_ACCESSIBILITY.bits()
        | Self::DESCENDANT_REQUESTED_ACCESSIBILITY.bits()
        | Self::REQUEST_PAINT.bits()
        | Self::TREE_CHANGED.bits();
}

WidgetState struct

pub(crate) struct WidgetState {
    pub(crate) id: Id,
    pub(crate) flags: PodFlags,
    pub(crate) origin: Point,
    pub(crate) parent_window_origin: Point,
    pub(crate) size: Size,
    pub(crate) sub_tree: Bloom<Id>,
    pub(crate) cursor: Option<Cursor>,
    pub(crate) cursor_change: CursorChange,
    pub(crate) focus_chain: Vec<IdPath>,
    pub(crate) focus_change: Option<FocusChange>,
}

Event and Lifecycle Enums

pub enum Event {
    WindowConnected,
    WindowCloseRequested,
    WindowDisconnected,
    WindowScale(Scale),
    WindowSize(Size),
    MouseDown(MouseEvent),
    MouseUp(MouseEvent),
    MouseMove(MouseEvent),
    MouseWheel(MouseEvent),
    MouseLeft(),
    KeyDown(KeyEvent),
    KeyUp(KeyEvent),
    Paste(Clipboard),
    Zoom(f64),
    Timer(TimerToken),
    AnimFrame(u64),
    TargetedAccessibilityAction(accesskit::ActionRequest),
    ImeStateChange
}
pub enum LifeCycle {
    HotChanged(bool),
    BuildFocusChain,
    FocusChanged(bool),
    DisabledChanged(bool),
    ViewContextChanged(ViewContext),
    Size(Size),
    TreeUpdate,
}

App struct

pub struct App<T, V: View<T>> {
    req_chan: tokio::sync::mpsc::Sender<AppReq>,
    response_chan: tokio::sync::mpsc::Receiver<RenderResponse<V, V::State>>,
    return_chan: tokio::sync::mpsc::Sender<(V, V::State, HashSet<Id>)>,
    id: Option<Id>,
    events: Vec<Message>,
    window_handle: WindowHandle,
    root_state: WidgetState,
    root_pod: Option<Pod>,
    size: Size,
    new_size: Size,
    cursor_pos: Option<Point>,
    focus: Option<IdPath>
    pub(crate) last_anim: Option<Instant>,
    pub(crate) timers: HashMap<TimerToken, WidgetId>,
    cx: Cx,
    font_cx: FontContext,
    pub(crate) rt: Runtime,
    window_id: crate::id::Id,
    pub(crate) accesskit_connected: bool,
    node_classes: accesskit::NodeClassSet,
    pub(crate) pending_text_registrations: Vec<TextFieldRegistration>,
    pub(crate) ime_handlers: Vec<(TextFieldToken, TextFieldRegistration)>,
    pub(crate) ime_focus_change: Option<Option<TextFieldToken>>,
}

Open Questions:

  • Is there any special handling that needs to be made for the Paste(Clipboard), and Zoom(f64)?
  • Will Command and Notification be a thing in Xilem? I assume that messages will replace both mechanisms?
  • Are there any new events that need to be added?
  • Is the WidgetAdded lifecycle needed here?
  • Should InternalEvent and InternalLifeCycle be a thing in Xilem as well?
  • Is the simplification of the framework structs in Xilem intentional or a consequence of not having all the functionality Druid offers?
  • How do we decide what should be a lifecycle and what should be an event? My intuition tell me that anything generated externally to the framework (e.g. Glazier) should be an event. Anything generated by the framework should be a lifecycle.
  • Why does the key_down() return a bool?
  • Since the definitions of mouse is changed to pointer would it make sense to update glazier in the same PR?
  • Will the request_paint_rect() still be useful?
  • Which bits should be in both? Should I leave previous bit positions unchanged or can I rearrange stuff for ChangeFlags and PodFlags?
  • What other fields do we need in the WidgetState?
@raphlinus
Copy link

Some of these deserve deeper answers but I'm going to

  • I don't have any info at the top of my head about Paste and Zoom.

  • I think most of the uses of Command and Notification go away, as the primary method to update the widget tree is through the build and rebuild methods, and the view tree has its own set of mechanisms for moving data around. That said, we probably do need a way to route commands to widgets internal to the tree, for example for focus tabbing. I don't think we need to preserve this from Druid.

  • Re WidgetAdded I think I'll refer to xilem#54. One motivating use case for tracking widget tree structure is focus tabbing.

  • Not sure about InternalEvent and InternalLifeCycle. I think this is part of the lifecycle/event question.

  • The simplification is partially intentional. Basically if we can get rid of complexity, we should. The process of starting a new codebase, adapting things from the old one, is a way to do that.

  • The return value of key_down says whether the event is handled. On Windows, if this returns false, then the default windowproc is called. I don't have the specific use cases at the top of my mind, but in general I think it's good for there to be a chain, and when a handler in the chain does process the key, further dispatching is halted.

  • Yes, it would be good to get all mouse stuff changed to pointer as soon as possible.

  • request_paint_rect is part of the design for partial invalidation. Here's a Zulip thread with a bit more info. We do want to track damage regions and provide for partial invalidation.

  • It's fine to rearrange bits in ChangeFlags and PodFlags, people shouldn't be relying on the internal details.

Feel free to keep asking questions, here, on Zulip, and in office hours.

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