feat(hotkeys): lay the shortcuts cheatsheet out in three columns

The read-only shortcuts modal listed every category in one tall column that
scrolled. Split the groups round-robin into three column models (Rust) and
render them side by side; the panel widened to fit. The editable customize
editor still uses the full single-column list.

cc
This commit is contained in:
vicrodh committed 2026-06-30 14:07:50 -06:00
1 parent e44aa07acf
commit 1a9c132760
3 files changed
+101 -66

No files matched your search

@@ -10,9 +10,71 @@
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { Radius } from "../foundation/radius.slint";
import { KeyboardShortcutsState, KeybindingsState } from "../state.slint";
import { KeyboardShortcutsState, KeybindingsState, KeybindingCategoryGroup } from "../state.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
// One category block (uppercase header + its binding rows). Extracted so the
// body can lay the groups out across THREE columns instead of one tall
// scrolling column.
component KbGroupBlock inherits VerticalLayout {
in property <KeybindingCategoryGroup> group;
spacing: 12px;
VerticalLayout {
spacing: 8px;
Text {
text: group.label;
color: Theme.text-muted;
font-size: Typography.legal;
font-weight: Typography.semibold;
}
Rectangle {
height: 1px;
background: Theme.border-subtle;
}
}
for entry in group.rows: Rectangle {
height: entry-row.preferred-height + 16px;
border-radius: Radius.sm;
background: Theme.surface-elevated;
entry-row := HorizontalLayout {
padding-left: 12px;
padding-right: 12px;
padding-top: 8px;
padding-bottom: 8px;
spacing: 12px;
Text {
text: entry.label;
color: Theme.text-primary;
font-size: Typography.body;
horizontal-stretch: 1;
vertical-alignment: center;
}
// Keycap — a subtle key-like chip (Radius.sm, NOT a pill, ADR-008).
Rectangle {
width: keycap-label.preferred-width + 16px;
height: keycap-label.preferred-height + 8px;
border-radius: Radius.sm;
background: Theme.surface-card;
border-width: 1px;
border-color: Theme.border-muted;
keycap-label := Text {
text: entry.shortcut;
color: Theme.text-secondary;
font-size: Typography.legal;
font-family: "monospace";
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
}
}
}
}
}
export component KeyboardShortcutsModal inherits Rectangle {
visible: KeyboardShortcutsState.open;
@@ -37,7 +99,7 @@ export component KeyboardShortcutsModal inherits Rectangle {
+ footer-row.preferred-height + 2px;
property <length> content-height: chrome-height + body.preferred-height + 40px;
width: Math.min(root.width - 80px, 660px);
width: Math.min(root.width - 80px, 980px);
height: Math.min(self.content-height, root.height - 80px);
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
@@ -131,70 +193,26 @@ export component KeyboardShortcutsModal inherits Rectangle {
viewport-width: self.width;
viewport-height: body.preferred-height;
body := VerticalLayout {
// Groups laid out across THREE columns (round-robin by
// index) so the cheatsheet fits without a tall scroll.
body := HorizontalLayout {
padding: 20px;
spacing: 24px;
for group in KeybindingsState.groups: VerticalLayout {
spacing: 12px;
// Uppercase category header + underline.
VerticalLayout {
spacing: 8px;
Text {
// Already localized in Rust; render as-is.
text: group.label;
color: Theme.text-muted;
font-size: Typography.legal;
font-weight: Typography.semibold;
}
Rectangle {
height: 1px;
background: Theme.border-subtle;
}
}
// One row per binding: label LEFT, keycap RIGHT.
for entry in group.rows: Rectangle {
height: entry-row.preferred-height + 16px;
border-radius: Radius.sm;
background: Theme.surface-elevated;
entry-row := HorizontalLayout {
padding-left: 12px;
padding-right: 12px;
padding-top: 8px;
padding-bottom: 8px;
spacing: 12px;
Text {
text: entry.label;
color: Theme.text-primary;
font-size: Typography.body;
horizontal-stretch: 1;
vertical-alignment: center;
}
// Keycap — a subtle key-like chip
// (Radius.sm, NOT a pill per ADR-008).
Rectangle {
width: keycap-label.preferred-width + 16px;
height: keycap-label.preferred-height + 8px;
border-radius: Radius.sm;
background: Theme.surface-card;
border-width: 1px;
border-color: Theme.border-muted;
keycap-label := Text {
text: entry.shortcut;
color: Theme.text-secondary;
font-size: Typography.legal;
font-family: "monospace";
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
}
}
}
}
VerticalLayout {
alignment: start;
spacing: 24px;
for group in KeybindingsState.groups-col1: KbGroupBlock { group: group; }
}
VerticalLayout {
alignment: start;
spacing: 24px;
for group in KeybindingsState.groups-col2: KbGroupBlock { group: group; }
}
VerticalLayout {
alignment: start;
spacing: 24px;
for group in KeybindingsState.groups-col3: KbGroupBlock { group: group; }
}
}
}
+5
View File
@@ -5254,6 +5254,11 @@ export struct KeybindingCategoryGroup {
export global KeybindingsState {
// Ordered category groups, rebuilt in Rust whenever a binding changes.
in property <[KeybindingCategoryGroup]> groups: [];
// Same groups split round-robin into three columns, for the read-only
// cheatsheet's 3-column layout (the editor still uses the full `groups`).
in property <[KeybindingCategoryGroup]> groups-col1: [];
in property <[KeybindingCategoryGroup]> groups-col2: [];
in property <[KeybindingCategoryGroup]> groups-col3: [];
// Count of actions whose binding differs from default ("N modified").
in property <int> modified-count: 0;
+15 -3
View File
@@ -324,7 +324,7 @@ fn reset_all() {
// Slint model (cheatsheet + customize editor share the groups)
// ============================================================================
fn build_groups() -> slint::ModelRc<crate::KeybindingCategoryGroup> {
fn build_group_vec() -> Vec<crate::KeybindingCategoryGroup> {
use slint::{ModelRc, VecModel};
let bindings = active_bindings();
let mut groups: Vec<crate::KeybindingCategoryGroup> = Vec::new();
@@ -346,7 +346,7 @@ fn build_groups() -> slint::ModelRc<crate::KeybindingCategoryGroup> {
rows: ModelRc::new(VecModel::from(rows)),
});
}
ModelRc::new(VecModel::from(groups))
groups
}
fn modified_count() -> i32 {
@@ -360,8 +360,20 @@ fn modified_count() -> i32 {
/// Repopulate the Slint state from the persisted bindings. Call at startup and
/// after any change.
pub fn refresh(window: &AppWindow) {
use slint::{ModelRc, VecModel};
let state = window.global::<KeybindingsState>();
state.set_groups(build_groups());
let groups = build_group_vec();
// Full list (customize editor renders one column) + a round-robin split into
// three columns for the read-only cheatsheet (avoids one tall scroll).
let mut cols: [Vec<crate::KeybindingCategoryGroup>; 3] = Default::default();
for (i, g) in groups.iter().enumerate() {
cols[i % 3].push(g.clone());
}
let [c0, c1, c2] = cols;
state.set_groups(ModelRc::new(VecModel::from(groups)));
state.set_groups_col1(ModelRc::new(VecModel::from(c0)));
state.set_groups_col2(ModelRc::new(VecModel::from(c1)));
state.set_groups_col3(ModelRc::new(VecModel::from(c2)));
state.set_modified_count(modified_count());
}