Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ pub enum Message {
AddSoundDirectory,
SoundDirectoryPickResult(Option<std::path::PathBuf>),
RemoveSoundDirectory(std::path::PathBuf),
// Appearance
ThemeChanged(theme::Theme),
}

impl Message {
Expand Down Expand Up @@ -581,6 +583,13 @@ impl HonkHonk {
}
self.update(Message::RescanLibrary)
}
Message::ThemeChanged(t) => {
self.config.theme = t;
if let Err(e) = self.config.save() {
eprintln!("honkhonk: config save error: {e}");
}
Task::none()
}
Comment thread
thewrz marked this conversation as resolved.
}
}

Expand Down Expand Up @@ -706,7 +715,10 @@ impl HonkHonk {
}

pub fn theme(&self) -> Theme {
Theme::Dark
match self.config.theme {
theme::Theme::Light => Theme::Light,
theme::Theme::Dark | theme::Theme::System => Theme::Dark,
}
}

pub fn subscription(&self) -> Subscription<Message> {
Expand Down Expand Up @@ -782,7 +794,7 @@ impl HonkHonk {
}

fn view_main(&self) -> Element<'_, Message> {
let t = theme::Theme::Dark;
let t = self.config.theme;
let header = self.view_header(t);
let chips = self.view_category_chips(t);
let filtered = self.filtered_sounds();
Expand Down Expand Up @@ -844,7 +856,7 @@ impl HonkHonk {
match self.view_mode {
ViewMode::Main => self.view_main(),
ViewMode::SlotManager => {
let t = theme::Theme::Dark;
let t = self.config.theme;
slot_manager::view_slot_manager(
&self.slots,
&self.slot_triggers,
Expand All @@ -854,7 +866,7 @@ impl HonkHonk {
)
}
ViewMode::Settings => {
crate::ui::settings::view_settings(self, crate::ui::theme::Theme::Dark)
crate::ui::settings::view_settings(self, self.config.theme)
}
}
}
Expand Down Expand Up @@ -1307,4 +1319,14 @@ mod tests {
let _ = app.update(Message::SoundDirectoryPickResult(None));
assert_eq!(app.config.sound_directories, before);
}

#[test]
fn theme_changed_updates_config() {
let mut app = HonkHonk::new_for_test();
assert_eq!(app.config.theme, crate::ui::theme::Theme::Dark);
let _ = app.update(Message::ThemeChanged(crate::ui::theme::Theme::Light));
assert_eq!(app.config.theme, crate::ui::theme::Theme::Light);
let _ = app.update(Message::ThemeChanged(crate::ui::theme::Theme::System));
assert_eq!(app.config.theme, crate::ui::theme::Theme::System);
}
}
35 changes: 26 additions & 9 deletions src/settings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,22 @@ pub struct SettingDef {
pub control: ControlType,
}

/// Phase 2: only RescanLibrary wired.
/// Add a SettingDef here when its backend sub-MVP lands.
pub static SETTINGS_REGISTRY: &[SettingDef] = &[SettingDef {
id: SettingId::RescanLibrary,
category: SettingCategory::Library,
label: "Scan now",
hint: "Force a re-scan of all sound folders.",
control: ControlType::Button,
}];
pub static SETTINGS_REGISTRY: &[SettingDef] = &[
SettingDef {
id: SettingId::Theme,
category: SettingCategory::Appearance,
label: "Theme",
hint: "Light, Dark, or follow your desktop environment.",
control: ControlType::Radio(&["Light", "Dark", "System"]),
},
SettingDef {
id: SettingId::RescanLibrary,
category: SettingCategory::Library,
label: "Scan now",
hint: "Force a re-scan of all sound folders.",
control: ControlType::Button,
},
];

#[cfg(test)]
mod tests {
Expand Down Expand Up @@ -100,4 +107,14 @@ mod tests {
.count();
assert_eq!(count, 0, "No audio settings wired in Phase 2 shell");
}

#[test]
fn theme_entry_exists_in_appearance_category() {
let def = SETTINGS_REGISTRY
.iter()
.find(|d| matches!(d.id, SettingId::Theme))
.expect("Theme must be in SETTINGS_REGISTRY");
assert!(matches!(def.category, SettingCategory::Appearance));
assert!(matches!(def.control, ControlType::Radio(_)));
}
}
6 changes: 6 additions & 0 deletions src/state/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::path::PathBuf;
use serde::{Deserialize, Serialize};

use crate::state::error::ConfigError;
use crate::ui::theme::Theme;

const DEFAULT_VOLUME: f32 = 0.85;
const DEFAULT_WIDTH: u32 = 900;
Expand All @@ -17,6 +18,8 @@ pub struct AppConfig {
pub volume: f32,
pub window_width: u32,
pub window_height: u32,
#[serde(default)]
pub theme: Theme,
}

impl Default for AppConfig {
Expand All @@ -34,6 +37,7 @@ impl Default for AppConfig {
volume: DEFAULT_VOLUME,
window_width: DEFAULT_WIDTH,
window_height: DEFAULT_HEIGHT,
theme: Theme::Dark,
}
}
}
Expand Down Expand Up @@ -151,6 +155,7 @@ mod tests {
volume: 0.5,
window_width: 1024,
window_height: 768,
theme: Theme::Dark,
};

let json = serde_json::to_string_pretty(&config).unwrap();
Expand All @@ -169,6 +174,7 @@ mod tests {
volume: 0.7,
window_width: 800,
window_height: 500,
theme: Theme::Dark,
};

config.save_to(&path).unwrap();
Expand Down
63 changes: 48 additions & 15 deletions src/ui/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ fn settings_content<'a>(state: &'a HonkHonk, t: Theme) -> Element<'a, Message> {
SettingsSection::Audio => view_audio_section(state, t),
SettingsSection::Library => view_library_section(state, t),
SettingsSection::Hotkeys => view_hotkeys_section(state, t),
SettingsSection::Appearance => view_appearance_section(t),
SettingsSection::Appearance => view_appearance_section(state, t),
SettingsSection::About => view_about_section(t),
};

Expand Down Expand Up @@ -180,6 +180,35 @@ pub fn render_setting_row<'a>(
})
.into()
}
(ControlType::Radio(options), SettingValue::Index(current)) => {
let id = def.id;
options
.iter()
.enumerate()
.fold(row![].spacing(theme::space::XS), |r, (i, label)| {
let msg = setting_message(id, SettingValue::Index(i));
let active = i == current;
r.push(
button(
text(*label)
.size(theme::font::BODY)
.color(if active { t.bg() } else { t.ink() }),
)
.on_press(msg)
.padding([6.0, 14.0])
.style(move |_t, _s| button::Style {
background: Some(theme::bg_color(if active {
t.ink()
} else {
t.panel()
})),
border: theme::tile_border(t.hairline2(), 1.0),
..Default::default()
}),
)
})
.into()
}
_ => text("—")
.size(theme::font::BODY)
.color(t.ink_faint())
Expand All @@ -197,27 +226,24 @@ pub fn render_setting_row<'a>(
.into()
}

/// Read the current value of a setting from app state.
/// Add arms here when backend sub-MVPs land.
pub fn get_setting_value(id: SettingId, _state: &HonkHonk) -> SettingValue {
pub fn get_setting_value(id: SettingId, state: &HonkHonk) -> SettingValue {
match id {
SettingId::RescanLibrary => SettingValue::None,
SettingId::Theme => SettingValue::Index(state.config.theme.setting_index()),
_ => SettingValue::None,
}
}

/// Map a setting id + value to the specific Message that applies it.
/// Add arms here when backend sub-MVPs land.
pub fn setting_message(id: SettingId, _value: SettingValue) -> Message {
match id {
SettingId::RescanLibrary => Message::RescanLibrary,
// All other IDs are unwired stubs — no SettingDef renders them yet.
// If this arm fires, a SettingDef was added without updating this function.
pub fn setting_message(id: SettingId, value: SettingValue) -> Message {
match (id, value) {
(SettingId::RescanLibrary, _) => Message::RescanLibrary,
(SettingId::Theme, SettingValue::Index(i)) => {
Message::ThemeChanged(crate::ui::theme::Theme::from_setting_index(i))
}
other => {
// Safety net: if this fires, a SettingDef was added without updating this function.
debug_assert!(
false,
"setting_message: unhandled SettingId {:?} — add an arm here when wiring a backend",
"setting_message: unhandled ({:?}) — add an arm here when wiring a backend",
other
);
Message::RescanLibrary
Expand Down Expand Up @@ -602,11 +628,18 @@ pub fn view_hotkeys_section<'a>(state: &'a HonkHonk, t: Theme) -> Element<'a, Me
)
}

pub fn view_appearance_section(t: Theme) -> Element<'static, Message> {
pub fn view_appearance_section<'a>(state: &'a HonkHonk, t: Theme) -> Element<'a, Message> {
let registry_rows = SETTINGS_REGISTRY
.iter()
.filter(|d| matches!(d.category, SettingCategory::Appearance))
.fold(column![].spacing(0.0), |col, def| {
col.push(render_setting_row(def, state, t))
});

section_layout(
"Appearance",
"How honky should HonkHonk look today?",
column![].into(),
registry_rows.into(),
t,
)
}
Expand Down
37 changes: 27 additions & 10 deletions src/ui/theme.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
use iced::{Background, Border, Color};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub enum Theme {
#[default]
Dark,
Light,
System,
}

impl Theme {
pub fn is_dark(self) -> bool {
matches!(self, Theme::Dark)
matches!(self, Theme::Dark | Theme::System)
}

pub fn setting_index(self) -> usize {
match self {
Theme::Light => 0,
Theme::Dark => 1,
Theme::System => 2,
}
}

pub fn from_setting_index(i: usize) -> Self {
match i {
0 => Theme::Light,
1 => Theme::Dark,
_ => Theme::System,
}
}
}

Expand Down Expand Up @@ -146,49 +163,49 @@ impl Hh for Theme {
fn bg(self) -> Color {
match self {
Theme::Light => hex(0xf4efe4),
Theme::Dark => hex(0x171410),
Theme::Dark | Theme::System => hex(0x171410),
}
}
fn panel(self) -> Color {
match self {
Theme::Light => hex(0xfffaf0),
Theme::Dark => hex(0x1f1c16),
Theme::Dark | Theme::System => hex(0x1f1c16),
}
}
fn ink(self) -> Color {
match self {
Theme::Light => hex(0x1a1208),
Theme::Dark => hex(0xfbf3df),
Theme::Dark | Theme::System => hex(0xfbf3df),
}
}
fn ink_dim(self) -> Color {
match self {
Theme::Light => hex(0x6a553a),
Theme::Dark => hex(0xa39377),
Theme::Dark | Theme::System => hex(0xa39377),
}
}
fn ink_faint(self) -> Color {
match self {
Theme::Light => hex(0xa8957a),
Theme::Dark => hex(0x6a5b46),
Theme::Dark | Theme::System => hex(0x6a5b46),
}
}
fn hairline(self) -> Color {
match self {
Theme::Light => Color::from_rgba(0.0, 0.0, 0.0, 0.06),
Theme::Dark => Color::from_rgba(1.0, 1.0, 1.0, 0.06),
Theme::Dark | Theme::System => Color::from_rgba(1.0, 1.0, 1.0, 0.06),
}
}
fn hairline2(self) -> Color {
match self {
Theme::Light => Color::from_rgba(0.0, 0.0, 0.0, 0.12),
Theme::Dark => Color::from_rgba(1.0, 1.0, 1.0, 0.12),
Theme::Dark | Theme::System => Color::from_rgba(1.0, 1.0, 1.0, 0.12),
}
}
fn good(self) -> Color {
match self {
Theme::Light => hex(0x16a34a),
Theme::Dark => hex(0x4ade80),
Theme::Dark | Theme::System => hex(0x4ade80),
}
}
fn accent(self) -> Color {
Expand Down
Loading