@@ -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