Skip to content

fix(ux): focus regression, keyboard room switching, and a filter that erased itself - #287

Merged
tyreseluo merged 3 commits into
mainfrom
fix/ux-top-three
Aug 5, 2026
Merged

fix(ux): focus regression, keyboard room switching, and a filter that erased itself#287
tyreseluo merged 3 commits into
mainfrom
fix/ux-top-three

Conversation

@tyreseluo

Copy link
Copy Markdown

Three unrelated UX problems, one commit each so they can be judged separately.

1. Keyboard focus repainted ghost buttons solid — regression from #285

#285 gave the base button color_focus: RBX_FOCUS_TINT, but that token is an opaque #0E8C9E and the shader replaces the fill outright:

fill = color_fill.mix(color_fill_focus, self.focus)

At focus = 1.0 the fill is the tint. Buttons carrying their own background were unaffected — which is why it got past review — but the ghost variants, whose color is transparent, became a solid accent block with a grey icon on it. Inverted, not outlined. Those are the composer tool buttons and the per-message copy button.

The base now focuses to RBX_ACCENT_SOFT. The token keeps its opaque value deliberately: making it translucent would let the page show through the solid variants instead.

The four semantic variants get an explicit color_focus equal to their own fill — left to inherit, a green confirm or red destructive button would wash teal on focus and lose the colour that identifies it. They already draw a border, so border_color_focus carries the signal for them.

2. The room quick switcher could not be driven by keyboard

Type a room name, press Enter, nothing happens. The results list had no concept of a selection — items only answered click_button.clicked — so switching rooms, the most frequent action in the client, always ended at the mouse.

Arrow keys move a selection, Enter opens it, and the selected row is tinted so the keys are not aiming blind.

  • Keys are claimed in Event::KeyDown, not via a Hit: focus stays in the query field while typing, so the list never receives hits of its own. Only Up/Down/Return are taken.
  • Selection clamps at the ends rather than wrapping, and resets when the result set changes — index 3 of the previous query has nothing to do with index 3 of this one.
  • The highlight is an Animator over a shader instance, not script_apply_eval!, which does nothing on FlatList template widgets (pitfall docs: add deployment guide for Robrix + Palpo + Octos #40). Both animator states are written every frame: rows are reused by index, so writing only on would strand the highlight on the old row.

3. A filter matching nothing brought the whole list back

Typing a room name that matched nothing restored the entire room list while the query was still in the box — the filter looked broken at the moment it was working.

update_displayed_rooms cleared display_filter and regenerated whenever every category came back empty. That also made the "no matching rooms" status unreachable: display_filter was already None by the time update_status ran, so the count fell into the unfiltered arm.

Why removing it is safe. The fallback dates from 73a27561 fix(search): improve people lookup and fallback empty local filters, when the sidebar filter and people search shared a path. They no longer do — the sidebar list is driven by MainFilterAction, while people results (RoomFilterResultTarget::RemoteUser) render in the quick switcher modal, a separate surface with its own list. Nothing depends on the sidebar staying populated.

One consequence worth naming: the newly reachable status string is English, like the five siblings it sits with. That file has no i18n at all yet, so the line is no more English than before — but it deserves its own change.

Testing

cargo test --lib --features agent_chat — 593 pass. typos src/ clean. Ran the app and confirmed no new runtime DSL/shader errors in the log, which is where a mistyped token or shader field surfaces (cargo check cannot see them).

By hand: Tab onto the copy and composer buttons gives a wash, not a block; green and red buttons keep their colour and gain a ring; the switcher moves with arrows, opens on Enter, clamps at both ends, and resets to the first row on a new query; a non-matching filter now leaves the list empty with "No matching rooms found".

#285 gave the base button `color_focus: RBX_FOCUS_TINT`, but that token is an
opaque `#0E8C9E` and the shader replaces the fill outright:

    fill = color_fill.mix(color_fill_focus, self.focus)

At focus=1.0 the fill *is* the tint. Buttons that carry their own background were
unaffected, which is why this got through review — but the ghost variants, whose
`color` is transparent, turned into a solid accent block with a grey icon on it.
Inverted, not outlined. Those are the composer tool buttons and the per-message
copy button: the two highest-traffic controls in the app.

The base now focuses to `RBX_ACCENT_SOFT`, a wash rather than a repaint. The
token keeps its opaque value on purpose; making *it* translucent would let the
page show through the solid variants instead.

The four semantic variants get an explicit `color_focus` equal to their own
fill. Left to inherit, a green confirm or a red destructive button would have
washed teal on focus and lost the colour that identifies it — they already draw
a border, so `border_color_focus` carries the signal for them.
Typing a room name and pressing Enter did nothing. The results list had no
concept of a selection — items only responded to `click_button.clicked` — so
switching rooms, the highest-frequency action in the client, always ended at the
mouse.

Arrow keys move a selection, Enter opens it, and the selected row is tinted so
the keys are not aiming blind.

Keys are claimed in `Event::KeyDown` rather than through a `Hit`: focus stays in
the query field while typing, so the list never receives hits of its own. Only
Up/Down/Return are taken; every other key still reaches the field.

Selection is clamped at the ends instead of wrapping — holding an arrow should
come to rest on the first or last result, not cycle — and resets whenever the
result set changes, since index 3 of the previous query has nothing to do with
index 3 of this one.

The highlight is an Animator over a shader instance, not `script_apply_eval!`,
which does nothing on widgets built from a FlatList template (pitfall #40). Both
animator states are written on every frame: rows are reused by index, so writing
only `on` would leave the old row highlighted after the selection moved.
Typing a room name that matched nothing brought the entire room list back while
the query was still sitting in the box — the filter looked broken at the moment
it was working correctly.

`update_displayed_rooms` cleared `display_filter` and regenerated whenever every
category came back empty. That also made the "no matching rooms" status
unreachable: by the time `update_status` ran, `display_filter` was already None,
so the count fell into the unfiltered arm.

The fallback dates from "improve people lookup and fallback empty local
filters", when the sidebar filter and people search shared a path. They no
longer do: the sidebar list is driven by `MainFilterAction`, while people
results (`RoomFilterResultTarget::RemoteUser`) are rendered by the quick
switcher modal, a separate surface with its own list. Nothing depends on the
sidebar staying populated.

Note the newly reachable status string is English, like the five siblings it
sits with — this file has no i18n at all yet, so the line is no more English
than before. Worth its own change.
@tyreseluo
tyreseluo merged commit 17516db into main Aug 5, 2026
12 checks passed
@tyreseluo
tyreseluo deleted the fix/ux-top-three branch August 5, 2026 05:17
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