Skip to content

Commit 5e8d961

Browse files
authored
Add tabbed settings (#88)
1 parent 5e7b00c commit 5e8d961

2 files changed

Lines changed: 190 additions & 32 deletions

File tree

src/main.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,10 @@ struct App {
243243
config: Config,
244244
/// Whether the settings panel is currently visible.
245245
show_settings: bool,
246+
/// Which tab of the Settings panel is currently in view. In-memory
247+
/// only — resets to `General` on each launch (intentional; see
248+
/// `SettingsTab` doc-comment).
249+
settings_tab: ui::SettingsTab,
246250
/// Whether the USBSID-Pico Device Config panel is currently visible.
247251
show_device_config: bool,
248252
/// Cached state for the Device Config panel. `None` means we haven't
@@ -579,6 +583,7 @@ impl App {
579583
sort_direction: SortDirection::Ascending,
580584
config,
581585
show_settings: false,
586+
settings_tab: ui::SettingsTab::General,
582587
show_device_config: false,
583588
device_cfg: None,
584589
device_cfg_status: String::new(),
@@ -1441,9 +1446,17 @@ impl App {
14411446
self.show_sid_panel = false;
14421447
self.show_device_config = false;
14431448
self.show_hvsc_browser = false;
1449+
// Reset the tab to General each time the panel is opened
1450+
// — the tab strip is a quick swap, and re-opening is
1451+
// when you want a fresh look at "everything".
1452+
self.settings_tab = ui::SettingsTab::General;
14441453
}
14451454
}
14461455

1456+
Message::SettingsTabChanged(t) => {
1457+
self.settings_tab = t;
1458+
}
1459+
14471460
Message::ToggleDeviceConfig => {
14481461
self.context_menu = None;
14491462
self.show_device_config = !self.show_device_config;
@@ -3158,6 +3171,7 @@ impl App {
31583171
&self.hvsc_sync_status,
31593172
self.hvsc_sync_progress,
31603173
self.sleep_selected_mins,
3174+
self.settings_tab,
31613175
);
31623176
column![
31633177
info_bar,

src/ui/mod.rs

Lines changed: 176 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,50 @@ pub struct DeviceConfigSnapshot {
9393
pub config: usbsid_pico_config::DeviceConfig,
9494
}
9595

96+
/// Which tab of the Settings panel is currently in view. Defaults to
97+
/// General on each launch — the tabs are cheap to switch and persisting
98+
/// this would hide the user's other pinned state (e.g. mid-typing in
99+
/// the HTTP proxy URL) when they reopen Settings.
100+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
101+
pub enum SettingsTab {
102+
#[default]
103+
General,
104+
Audio,
105+
Library,
106+
Network,
107+
Help,
108+
}
109+
110+
impl SettingsTab {
111+
pub fn label(self) -> &'static str {
112+
match self {
113+
SettingsTab::General => "⏯ General",
114+
SettingsTab::Audio => "🔊 Audio",
115+
SettingsTab::Library => "📚 Library",
116+
SettingsTab::Network => "🌐 Network",
117+
SettingsTab::Help => "❓ Help",
118+
}
119+
}
120+
121+
pub fn tip(self) -> &'static str {
122+
match self {
123+
SettingsTab::General => "Skip RSID · stereo · length · sleep · surprise · font size",
124+
SettingsTab::Audio => "Output engine · Ultimate 64 · macOS USB transport",
125+
SettingsTab::Library => "HVSC sync · Songlengths · STIL",
126+
SettingsTab::Network => "HTTP proxy · Remote-control server",
127+
SettingsTab::Help => "Keyboard shortcuts",
128+
}
129+
}
130+
131+
pub const ALL: [SettingsTab; 5] = [
132+
SettingsTab::General,
133+
SettingsTab::Audio,
134+
SettingsTab::Library,
135+
SettingsTab::Network,
136+
SettingsTab::Help,
137+
];
138+
}
139+
96140
// ─────────────────────────────────────────────────────────────────────────────
97141
// Messages
98142
// ─────────────────────────────────────────────────────────────────────────────
@@ -197,6 +241,9 @@ pub enum Message {
197241

198242
// Settings
199243
ToggleSettings,
244+
/// Switch which Settings tab is in view. In-memory only, no config
245+
/// persistence — see `SettingsTab` doc-comment.
246+
SettingsTabChanged(SettingsTab),
200247
ToggleSkipRsid,
201248
ToggleForceStereo2sid,
202249
/// macOS-only: switch USB transport between root bridge daemon and
@@ -3009,6 +3056,9 @@ pub fn settings_panel<'a>(
30093056
hvsc_sync_progress: Option<(u32, u32)>,
30103057
// Currently-armed sleep timer duration (minutes). `None` = disabled.
30113058
sleep_selected_mins: Option<u32>,
3059+
// Which tab is currently in view — selects which section subset is
3060+
// composed into the scrollable content column at the end.
3061+
active_tab: SettingsTab,
30123062
) -> Element<'a, Message> {
30133063
let header = row![
30143064
text("Settings")
@@ -3606,17 +3656,29 @@ pub fn settings_panel<'a>(
36063656
.spacing(6);
36073657

36083658
// ── Keyboard shortcuts ───────────────────────────────────────
3659+
// Kept in sync with the keyboard subscription in main.rs — every
3660+
// key the app actually handles is listed here. Groups: transport,
3661+
// navigation, mode toggles, volume, panels, meta.
36093662
let mut kb_col = column![text("Keyboard shortcuts:")
36103663
.size(font::sized(14.0))
36113664
.color(Color::from_rgb(0.75, 0.77, 0.82))]
36123665
.spacing(4);
36133666
for (key, desc) in [
36143667
("Space", "Play / Pause (when search inactive)"),
3615-
("← →", "Previous / Next track"),
3616-
("↑ ↓", "Navigate playlist"),
3668+
("← / →", "Previous / Next track"),
3669+
("↑ / ↓", "Select previous / next in playlist"),
3670+
("Delete", "Remove selected track"),
3671+
("H", "Toggle favourite for current track"),
3672+
("Shift+H", "Toggle shuffle"),
3673+
(", / .", "Nudge master volume −5% / +5%"),
3674+
("V", "Cycle visualiser (Bars / Scope / Tracker / Karaoke)"),
3675+
("F", "Toggle full-screen visualiser"),
3676+
("K", "Toggle karaoke lyrics (MUS files)"),
3677+
("M", "Toggle mini player"),
36173678
("L", "Toggle 📚 Library panel"),
3618-
("Delete", "Remove selected"),
36193679
("Ctrl+F", "Focus search"),
3680+
("?", "Show / hide help overlay"),
3681+
("Escape", "Close overlay / context menu"),
36203682
] {
36213683
kb_col = kb_col.push(
36223684
row![
@@ -3632,42 +3694,124 @@ pub fn settings_panel<'a>(
36323694
);
36333695
}
36343696

3697+
// ── Tab strip ───────────────────────────────────────────────
3698+
// Horizontal row of 5 pill buttons; the active tab gets a bright
3699+
// green background (same treatment as the sleep-timer picker
3700+
// above), the rest stay dim. Hovering any tab shows a one-line
3701+
// hint of what's inside — helpful before you know the layout.
3702+
let tab_button = |tab: SettingsTab| -> Element<'a, Message> {
3703+
let is_active = tab == active_tab;
3704+
let btn: Element<'a, Message> = button(text(tab.label()).size(font::sized(12.0)).color(
3705+
if is_active {
3706+
Color::from_rgb(0.10, 0.12, 0.15)
3707+
} else {
3708+
Color::from_rgb(0.80, 0.82, 0.90)
3709+
},
3710+
))
3711+
.on_press(Message::SettingsTabChanged(tab))
3712+
.padding(Padding::from([6, 14]))
3713+
.style(move |_theme: &Theme, st| {
3714+
let bg = if is_active {
3715+
match st {
3716+
button::Status::Hovered => Color::from_rgb(0.40, 0.85, 0.55),
3717+
button::Status::Pressed => Color::from_rgb(0.30, 0.70, 0.45),
3718+
_ => Color::from_rgb(0.35, 0.80, 0.50),
3719+
}
3720+
} else {
3721+
match st {
3722+
button::Status::Hovered => Color::from_rgb(0.22, 0.25, 0.30),
3723+
button::Status::Pressed => Color::from_rgb(0.15, 0.17, 0.20),
3724+
_ => Color::from_rgb(0.16, 0.18, 0.22),
3725+
}
3726+
};
3727+
button::Style {
3728+
background: Some(iced::Background::Color(bg)),
3729+
text_color: if is_active {
3730+
Color::from_rgb(0.10, 0.12, 0.15)
3731+
} else {
3732+
Color::from_rgb(0.80, 0.82, 0.90)
3733+
},
3734+
border: iced::Border {
3735+
radius: 4.0.into(),
3736+
width: 1.0,
3737+
color: if is_active {
3738+
Color::from_rgb(0.40, 0.85, 0.55)
3739+
} else {
3740+
Color::from_rgb(0.25, 0.27, 0.32)
3741+
},
3742+
},
3743+
..Default::default()
3744+
}
3745+
})
3746+
.into();
3747+
with_tip(btn, tab.tip())
3748+
};
3749+
3750+
let mut tab_strip: Row<'a, Message> = row![].spacing(6);
3751+
for &t in SettingsTab::ALL.iter() {
3752+
tab_strip = tab_strip.push(tab_button(t));
3753+
}
3754+
3755+
// ── Per-tab content ─────────────────────────────────────────
3756+
// Only the sections belonging to the active tab are pushed into
3757+
// the scrollable column. Every *_section is a cheap `Element`
3758+
// tree with no I/O — building all of them upfront and picking
3759+
// here is simpler than plumbing conditional construction.
3760+
let mut tab_content: Column<'a, Message> = column![].spacing(16);
3761+
match active_tab {
3762+
SettingsTab::General => {
3763+
tab_content = tab_content
3764+
.push(rsid_section)
3765+
.push(rule::horizontal(1))
3766+
.push(surprise_section)
3767+
.push(rule::horizontal(1))
3768+
.push(stereo_section)
3769+
.push(rule::horizontal(1))
3770+
.push(length_section)
3771+
.push(rule::horizontal(1))
3772+
.push(sleep_section)
3773+
.push(rule::horizontal(1))
3774+
.push(font_size_section);
3775+
}
3776+
SettingsTab::Audio => {
3777+
tab_content = tab_content
3778+
.push(engine_col)
3779+
.push(rule::horizontal(1))
3780+
.push(macos_usb_section);
3781+
}
3782+
SettingsTab::Library => {
3783+
tab_content = tab_content
3784+
.push(hvsc_section)
3785+
.push(rule::horizontal(1))
3786+
.push(dl_section)
3787+
.push(rule::horizontal(1))
3788+
.push(stil_section);
3789+
}
3790+
SettingsTab::Network => {
3791+
tab_content = tab_content
3792+
.push(proxy_section)
3793+
.push(rule::horizontal(1))
3794+
.push(remote_section);
3795+
}
3796+
SettingsTab::Help => {
3797+
tab_content = tab_content.push(kb_col);
3798+
}
3799+
}
3800+
3801+
// Header + tab strip stay anchored; only the tab body scrolls.
36353802
let content = column![
36363803
header,
36373804
rule::horizontal(1),
3638-
engine_col,
3639-
rule::horizontal(1),
3640-
macos_usb_section,
3641-
rule::horizontal(1),
3642-
rsid_section,
3643-
rule::horizontal(1),
3644-
surprise_section,
3645-
rule::horizontal(1),
3646-
stereo_section,
3647-
rule::horizontal(1),
3648-
length_section,
3649-
rule::horizontal(1),
3650-
font_size_section,
3651-
rule::horizontal(1),
3652-
sleep_section,
3805+
tab_strip,
36533806
rule::horizontal(1),
3654-
proxy_section,
3655-
rule::horizontal(1),
3656-
dl_section,
3657-
rule::horizontal(1),
3658-
stil_section,
3659-
rule::horizontal(1),
3660-
hvsc_section,
3661-
rule::horizontal(1),
3662-
remote_section,
3663-
rule::horizontal(1),
3664-
kb_col,
3807+
scrollable(tab_content.padding(Padding::from([4, 0]))).height(Length::Fill),
36653808
]
3666-
.spacing(16)
3809+
.spacing(12)
36673810
.padding(Padding::from([16, 24]))
3668-
.width(Length::Fill);
3811+
.width(Length::Fill)
3812+
.height(Length::Fill);
36693813

3670-
container(scrollable(content))
3814+
container(content)
36713815
.width(Length::Fill)
36723816
.height(Length::Fill)
36733817
.style(|_theme: &Theme| container::Style {

0 commit comments

Comments
 (0)