Skip to content

Commit ff4356e

Browse files
authored
Merge pull request #71 from tamirelazar/feat/ui-system
feat(ui): two-depth controls instrument + ambient surface + L2 widget/motion kit
2 parents 2e1a429 + 5bcdc9f commit ff4356e

29 files changed

Lines changed: 8251 additions & 2247 deletions

Cargo.lock

Lines changed: 98 additions & 72 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,17 @@ exclude = [
2222
default = ["terminal"]
2323
# gui is opt-in: cargo build --features gui
2424
gui = ["dep:iced", "dep:iced_term"]
25-
terminal = ["dep:crossterm", "dep:signal-hook", "dep:memory-stats", "dep:thiserror"]
25+
terminal = ["dep:crossterm", "dep:signal-hook", "dep:memory-stats", "dep:thiserror", "dep:tui-input"]
2626
# audio (choir mode) opt-in: cargo build --features audio
2727
audio = ["dep:cpal"]
2828
# multi-species is experimental: cargo build --features multi-species
2929
multi-species = []
3030

3131
[dependencies]
3232
crossterm = { version = "0.27", optional = true }
33+
# tui-input as a pure state machine (InputRequest API), default-features=false to
34+
# drop its event backend → no crossterm coupling. Drives the config-save text field.
35+
tui-input = { version = "0.15", optional = true, default-features = false }
3336
clap = { version = "4.4", features = ["derive"] }
3437
clap_complete = "4.4"
3538
rand = "0.8"

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,22 @@ lowercase increases and uppercase (Shift) decreases.
121121
| `q` | Quit |
122122
| `Esc` | Close open overlay |
123123

124+
### Controls panel navigation
125+
126+
The Controls panel opens into the Console study view (full master-detail interface) and supports two-depth navigation:
127+
128+
| Key | Action |
129+
|---|---|
130+
| `Tab` | Toggle between Tuner (bottom strip) and Console (full panel) |
131+
| `← / →` | Adjust the focused parameter (equivalent to its per-parameter hotkey) |
132+
| `↑ / ↓` | Move focus between parameters |
133+
| `` | Activate an action parameter (Reset / Save Frame / Randomize) |
134+
| `[ / ]` | Move to previous / next category |
135+
| `{ / }` | Adjust dither intensity (development feature) |
136+
| `Esc` | Close the panel |
137+
138+
Note: The per-parameter hotkeys (`a`/`A`, `j`/`J`, `t`/`T`, etc. from the Simulation and Appearance tables above) continue to work anywhere and take precedence. Arrow keys provide keyboard-driven alternatives to the focused-param hotkey while the panel is open.
139+
124140
### Custom Keybinds
125141

126142
Customize quick-keys `1``7` by creating a `~/.config/tslime/keybinds.toml` file with the following format:
@@ -209,7 +225,7 @@ tracking issue describing current state and what's needed — contributions welc
209225
| Choir mode (audio) | `cargo install tslime --features audio`, then `--choir` | #9 |
210226
| GUI mode | `cargo build --features gui` | #10 |
211227
| WASM build | `tslime-wasm/` (standalone crate) | #11 |
212-
| Dithering | hidden flags: `--dither-mode ordered` (and `d`/`D`/`[`/`]` keys once enabled) | #12 |
228+
| Dithering | hidden flags: `--dither-mode ordered` (and `d`/`D`/`{`/`}` keys once enabled) | #12 |
213229

214230
## Contributing
215231

src/app/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ use crate::render::dither::DitherMode;
1717
use crate::render::downsample::{downsample, DownsampledFrame};
1818
use crate::render::grid::{GridRenderer, GridStyle};
1919
use crate::render::overlay::{
20-
build_notification_panel, ConfigBrowserOverlay, ConfigSaveOverlay, DashboardOverlay,
21-
KeyboardHintsOverlay, PauseOverlay, PresetComparisonOverlay, RenderedOverlay,
20+
ConfigBrowserOverlay, ConfigSaveOverlay, DashboardOverlay, KeyboardHintsOverlay, PauseOverlay,
21+
PresetComparisonOverlay, RenderedOverlay,
2222
};
2323
use crate::render::palette::{hex_to_rgb, palette_accent_color, RgbColor};
2424
use crate::render::palette_editor::{

src/app/runner.rs

Lines changed: 1634 additions & 162 deletions
Large diffs are not rendered by default.

src/overlay/input_manager.rs

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,4 +260,170 @@ mod tests {
260260
OverlayInputResult::CloseOverlay
261261
);
262262
}
263+
264+
// ─── UX-win G: dismiss-key consistency ───────────────────────────────────
265+
266+
/// Esc closes every overlay that supports it (all except PauseBadge/PauseLogo).
267+
#[test]
268+
fn esc_closes_all_dismissable_overlays() {
269+
let esc = make_key(KeyCode::Esc, KeyModifiers::NONE);
270+
for overlay in [
271+
OverlayType::Controls,
272+
OverlayType::Dashboard,
273+
OverlayType::KeyboardHints,
274+
OverlayType::ConfigBrowser,
275+
OverlayType::ConfigSave,
276+
OverlayType::DirtyGuard,
277+
OverlayType::PresetComparison,
278+
] {
279+
let mut state = OverlayState::default();
280+
state.open(overlay);
281+
let result = OverlayInputManager::handle_input(&state, &esc);
282+
// PaletteEditor delegates internally; all others return CloseOverlay.
283+
assert_eq!(
284+
result,
285+
OverlayInputResult::CloseOverlay,
286+
"{overlay:?} + Esc should return CloseOverlay"
287+
);
288+
}
289+
}
290+
291+
/// Pause overlays do NOT close on Esc (they are not user-dismissable modals).
292+
#[test]
293+
fn esc_does_not_close_pause_overlays() {
294+
let esc = make_key(KeyCode::Esc, KeyModifiers::NONE);
295+
for overlay in [OverlayType::PauseBadge, OverlayType::PauseLogo] {
296+
let mut state = OverlayState::default();
297+
state.open(overlay);
298+
let result = OverlayInputManager::handle_input(&state, &esc);
299+
// Pause overlays don't close on Esc — the key is not consumed either.
300+
assert_ne!(
301+
result,
302+
OverlayInputResult::CloseOverlay,
303+
"{overlay:?} + Esc must NOT return CloseOverlay"
304+
);
305+
}
306+
}
307+
308+
/// PaletteEditor Esc is delegated to the internal handler (returns NotHandled
309+
/// so the runner's PaletteEditor block can do its mode-aware close logic).
310+
#[test]
311+
fn esc_on_palette_editor_is_delegated() {
312+
let mut state = OverlayState::default();
313+
state.open(OverlayType::PaletteEditor);
314+
let esc = make_key(KeyCode::Esc, KeyModifiers::NONE);
315+
assert_eq!(
316+
OverlayInputManager::handle_input(&state, &esc),
317+
OverlayInputResult::NotHandled,
318+
"PaletteEditor Esc must be delegated (NotHandled), not CloseOverlay"
319+
);
320+
}
321+
322+
/// Each overlay's opening toggle key re-closes it.
323+
#[test]
324+
fn toggle_key_re_closes_controls() {
325+
let mut state = OverlayState::default();
326+
state.open(OverlayType::Controls);
327+
for c in ['h', 'H'] {
328+
let k = make_key(KeyCode::Char(c), KeyModifiers::NONE);
329+
assert_eq!(
330+
OverlayInputManager::handle_input(&state, &k),
331+
OverlayInputResult::CloseOverlay,
332+
"'{c}' should re-close Controls"
333+
);
334+
}
335+
}
336+
337+
/// Dashboard dual-key: both `\` and `|` re-close when Dashboard is open.
338+
#[test]
339+
fn dashboard_dual_toggle_both_reclose() {
340+
let mut state = OverlayState::default();
341+
state.open(OverlayType::Dashboard);
342+
for c in ['\\', '|'] {
343+
let k = make_key(KeyCode::Char(c), KeyModifiers::NONE);
344+
assert_eq!(
345+
OverlayInputManager::handle_input(&state, &k),
346+
OverlayInputResult::CloseOverlay,
347+
"'{c}' should re-close Dashboard"
348+
);
349+
}
350+
}
351+
352+
/// KeyboardHints `?` re-closes when open.
353+
#[test]
354+
fn keyboard_hints_toggle_recloses() {
355+
let mut state = OverlayState::default();
356+
state.open(OverlayType::KeyboardHints);
357+
let k = make_key(KeyCode::Char('?'), KeyModifiers::NONE);
358+
assert_eq!(
359+
OverlayInputManager::handle_input(&state, &k),
360+
OverlayInputResult::CloseOverlay
361+
);
362+
}
363+
364+
/// ConfigBrowser Ctrl+L/B re-closes when open; plain l/b pass through.
365+
#[test]
366+
fn config_browser_toggle_recloses_ctrl_only() {
367+
let mut state = OverlayState::default();
368+
state.open(OverlayType::ConfigBrowser);
369+
for c in ['l', 'L', 'b', 'B'] {
370+
let ctrl = make_key(KeyCode::Char(c), KeyModifiers::CONTROL);
371+
assert_eq!(
372+
OverlayInputManager::handle_input(&state, &ctrl),
373+
OverlayInputResult::CloseOverlay,
374+
"Ctrl+{c} should re-close ConfigBrowser"
375+
);
376+
// Plain key must NOT close (it's not the toggle key)
377+
let plain = make_key(KeyCode::Char(c), KeyModifiers::NONE);
378+
assert_ne!(
379+
OverlayInputManager::handle_input(&state, &plain),
380+
OverlayInputResult::CloseOverlay,
381+
"plain '{c}' must not re-close ConfigBrowser"
382+
);
383+
}
384+
}
385+
386+
/// ConfigSave Ctrl+S re-closes when open.
387+
#[test]
388+
fn config_save_toggle_recloses() {
389+
let mut state = OverlayState::default();
390+
state.open(OverlayType::ConfigSave);
391+
let ctrl_s = make_key(KeyCode::Char('s'), KeyModifiers::CONTROL);
392+
assert_eq!(
393+
OverlayInputManager::handle_input(&state, &ctrl_s),
394+
OverlayInputResult::CloseOverlay
395+
);
396+
}
397+
398+
/// Esc when no overlay is open returns NotHandled (passes to error-MSG dismiss logic).
399+
#[test]
400+
fn esc_with_no_overlay_is_not_handled() {
401+
let state = OverlayState::default();
402+
let esc = make_key(KeyCode::Esc, KeyModifiers::NONE);
403+
assert_eq!(
404+
OverlayInputManager::handle_input(&state, &esc),
405+
OverlayInputResult::NotHandled,
406+
"Esc with no overlay open must be NotHandled (reaches sticky-error dismiss)"
407+
);
408+
}
409+
410+
/// Space (pause) is never consumed by the overlay input manager — it always passes through.
411+
#[test]
412+
fn space_never_consumed_by_overlay_manager() {
413+
let space = make_key(KeyCode::Char(' '), KeyModifiers::NONE);
414+
for overlay in [
415+
OverlayType::Controls,
416+
OverlayType::Dashboard,
417+
OverlayType::KeyboardHints,
418+
] {
419+
let mut state = OverlayState::default();
420+
state.open(overlay);
421+
let result = OverlayInputManager::handle_input(&state, &space);
422+
assert_ne!(
423+
result,
424+
OverlayInputResult::Consumed,
425+
"Space must never be Consumed ({overlay:?} open)"
426+
);
427+
}
428+
}
263429
}

src/overlay/mod.rs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,6 @@ pub enum OverlayType {
4444
PauseBadge,
4545
/// Pause logo overlay.
4646
PauseLogo,
47-
/// Notification toast overlay.
48-
Notification,
4947
}
5048

5149
impl OverlayType {
@@ -73,7 +71,6 @@ impl OverlayType {
7371
OverlayType::PauseBadge => 1,
7472
OverlayType::Controls => 2,
7573
OverlayType::Dashboard => 3,
76-
OverlayType::Notification => 4,
7774
OverlayType::Help => 10,
7875
OverlayType::ConfigBrowser => 10,
7976
OverlayType::ConfigSave => 10,
@@ -147,8 +144,6 @@ pub struct OverlayCollection<'a> {
147144
pub controls: Option<(&'a RenderedOverlay, usize, usize)>,
148145
/// Dashboard overlay.
149146
pub dashboard: Option<(&'a RenderedOverlay, usize, usize)>,
150-
/// Notification overlay.
151-
pub notification: Option<(&'a RenderedOverlay, usize, usize)>,
152147
/// Config browser overlay.
153148
pub config_browser: Option<(&'a RenderedOverlay, usize, usize)>,
154149
/// Config save overlay.
@@ -187,11 +182,6 @@ impl<'a> OverlayCollection<'a> {
187182
self.dashboard,
188183
OverlayType::Dashboard.z_order(),
189184
),
190-
(
191-
OverlayType::Notification,
192-
self.notification,
193-
OverlayType::Notification.z_order(),
194-
),
195185
(
196186
OverlayType::ConfigBrowser,
197187
self.config_browser,

0 commit comments

Comments
 (0)