Skip to content

Comments

masonry_core: Add first-class pseudo-class state on WidgetState#1669

Open
waywardmonkeys wants to merge 2 commits intolinebender:mainfrom
waywardmonkeys:sketch/pseudos
Open

masonry_core: Add first-class pseudo-class state on WidgetState#1669
waywardmonkeys wants to merge 2 commits intolinebender:mainfrom
waywardmonkeys:sketch/pseudos

Conversation

@waywardmonkeys
Copy link
Contributor

Motivation

Masonry tracks interaction states (hovered, active, focused, disabled) as individual booleans on WidgetState. When these change, the framework sets request_pre_paint and needs_paint, then calls widget.update(). But request_paint (for Widget::paint()) is not set by the framework— widgets must call ctx.request_paint_only() in their update() to get paint() re-run. This is fragile: every widget that changes appearance on hover/focus/active must remember to do this, and forgetting causes rendering bugs that are easy to miss.

There's also no unified state representation that a future style/selector system could match against. CSS-like selectors (:hover, :focus, :disabled, :checked) need a single queryable bitfield, not scattered booleans.

Changes

Introduce PseudoId(u8) and PseudoSet(u64) as a compact bitfield on WidgetState that mirrors the existing boolean interaction flags:

  • Built-in pseudo IDs: HOVER(0), ACTIVE(1), FOCUS(2), FOCUS_WITHIN(3), DISABLED(4). Bits 5–63 are available for widget-defined states.
  • Context API: ctx.set_pseudo(id, value) on mutable contexts auto-invalidates pre_paint + paint when state changes. ctx.pseudos() and ctx.has_pseudo(id) on all contexts for reads.
  • Framework wiring: The update pass now sets pseudo bits alongside the existing boolean flags at all five interaction sites (hover, active, focus, focus-within, disabled). Also adds the missing request_paint = true at each site, fixing the root fragility.
  • Widget-defined state: PSEUDO_TOGGLED(5) for checkboxes and switches. Checkbox::set_checked and Switch::set_on now use ctx.set_pseudo() instead of request_render(), and their update() methods no longer need manual request_paint_only() calls.

Next steps

  • Style/selector matching: With pseudo state unified in a bitfield, a selector engine can match :hover, :active, :focus, :disabled, :checked etc. against widget_state.pseudos in a single bitmask comparison.
  • Pseudo registry: A runtime registry for widget/app-defined pseudo IDs (allocating from bits 5+) so names can be used in stylesheets.
  • Migrate remaining booleans: The existing is_hovered, is_active, etc. booleans could eventually be replaced by pseudo-set queries, reducing WidgetState size.
  • Broader widget adoption: Other toggle-like widgets (disclosure panels, radio buttons) can adopt PSEUDO_TOGGLED or define their own pseudo states.

@waywardmonkeys
Copy link
Contributor Author

@PoignardAzur This is a demonstration of what we talked about yesterday.

// Checked state impacts appearance and accessibility node
this.ctx.request_render();
this.ctx.set_pseudo(PSEUDO_TOGGLED, checked);
this.ctx.request_accessibility_update();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was calling request_render before, but now relies on what set_pseudo does, the accessibility (and a post-paint) are the missing bits. This widget doesn't need a post-paint.

Masonry tracks interaction states (hovered, active, focused, disabled) as
individual booleans on `WidgetState`. When these change, the framework sets
`request_pre_paint` and `needs_paint`, then calls `widget.update()`. But
`request_paint` (for `Widget::paint()`) is **not** set by the framework—
widgets must call `ctx.request_paint_only()` in their `update()` to get
`paint()` re-run. This is fragile: every widget that changes appearance on
hover/focus/active must remember to do this, and forgetting causes
rendering bugs that are easy to miss.

There's also no unified state representation that a future style/selector
system could match against. CSS-like selectors (`:hover`, `:focus`,
`:disabled`, `:checked`) need a single queryable bitfield, not scattered
booleans.

Introduce `PseudoId(u8)` and `PseudoSet(u64)` as a compact bitfield on
`WidgetState` that mirrors the existing boolean interaction flags:

- **Built-in pseudo IDs:** `HOVER(0)`, `ACTIVE(1)`, `FOCUS(2)`,
  `FOCUS_WITHIN(3)`, `DISABLED(4)`. Bits 5–63 are available for
  widget-defined states.
- **Context API:** `ctx.set_pseudo(id, value)` on mutable contexts
  auto-invalidates `pre_paint` + `paint` when state changes.
  `ctx.pseudos()` and `ctx.has_pseudo(id)` on all contexts for reads.
- **Framework wiring:** The update pass now sets pseudo bits alongside
  the existing boolean flags at all five interaction sites (hover, active,
  focus, focus-within, disabled). Also adds the missing `request_paint =
  true` at each site, fixing the root fragility.
- **Widget-defined state:** `PSEUDO_TOGGLED(5)` for checkboxes and
  switches. `Checkbox::set_checked` and `Switch::set_on` now use
  `ctx.set_pseudo()` instead of `request_render()`, and their `update()`
  methods no longer need manual `request_paint_only()` calls.

- **Style/selector matching:** With pseudo state unified in a bitfield,
  a selector engine can match `:hover`, `:active`, `:focus`, `:disabled`,
  `:checked` etc. against `widget_state.pseudos` in a single bitmask
  comparison.
- **Pseudo registry:** A runtime registry for widget/app-defined pseudo
  IDs (allocating from bits 5+) so names can be used in stylesheets.
- **Migrate remaining booleans:** The existing `is_hovered`, `is_active`,
  etc. booleans could eventually be replaced by pseudo-set queries,
  reducing `WidgetState` size.
- **Broader widget adoption:** Other toggle-like widgets (disclosure
  panels, radio buttons) can adopt `PSEUDO_TOGGLED` or define their own
  pseudo states.
self.widget_state.request_paint = true;
self.widget_state.needs_paint = true;
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the future, it would be up to the style engine to decide what needs requesting. This is just a step along the way.

/// Pseudo-class for toggled widgets (checkboxes, switches, etc.).
///
/// Widget-defined pseudo state at bit 5.
pub const PSEUDO_TOGGLED: core::PseudoId = core::PseudoId(5);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is an example of doing it outside of PseudoId and would, in the future, be done with a registry. But not sure where to hang that registry from.

Now that the framework's update passes set `request_paint` alongside
`request_pre_paint` and `needs_paint` for all interaction state changes
(including `ChildFocusChanged`, `ChildHoveredChanged`, and
`ChildActiveChanged` on ancestor widgets), individual widgets no longer
need to manually call `request_paint_only()` or `request_pre_paint()`
in their `update()` methods for these events.

Widgets removed from:
- `Label`: `DisabledChanged` → `request_paint_only()`
- `Badge`: `DisabledChanged` → `request_paint_only()`
- `TextInput`: `ChildFocusChanged` → `request_pre_paint()`
- `Split`: `FocusChanged/HoveredChanged/ActiveChanged/DisabledChanged`
  → `request_paint_only()`

Widgets NOT changed (these are widget-specific logic, not interaction
state responses):
- `Button`: pointer capture in `on_pointer_event`
- `Spinner`: animation frame paint
- `TextArea`: cursor blink, window focus selection color
- `Label`/`TextArea`: `set_hint()` mutators
- `Split`: `set_draggable()`/`set_bar_solid()` mutators
- Property-change handlers in checkbox, progress_bar, divider, slider
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.

1 participant