From 5d15cde6754be1b4a28fe39ede3686f2425cd3b1 Mon Sep 17 00:00:00 2001 From: Bjorn Blomberg Date: Wed, 15 Jul 2026 09:39:39 +0200 Subject: [PATCH] Introduce central theme module; rounded borders and consistent styling All colors/styles now live in src/theme.rs (palette + pane/dialog block builders): sidebar, file view, bottom bar, context menu and all dialogs share one look that can be changed in one place. Rounded borders throughout. Co-Authored-By: Claude Fable 5 --- src/main.rs | 1 + src/theme.rs | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/ui.rs | 85 ++++++++++------------------------------------ 3 files changed, 114 insertions(+), 67 deletions(-) create mode 100644 src/theme.rs diff --git a/src/main.rs b/src/main.rs index 07bda4d..d9b268e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod app; mod config; mod file_ops; +mod theme; mod ui; mod events; diff --git a/src/theme.rs b/src/theme.rs new file mode 100644 index 0000000..b8b0d5e --- /dev/null +++ b/src/theme.rs @@ -0,0 +1,95 @@ +//! Centralt tema för TUI-FM. +//! +//! ALLA färger och återkommande stilar definieras här — ändra utseendet +//! på hela appen från en enda fil. UI-koden ska aldrig hårdkoda +//! `Color::`-literaler för sådant som temat täcker. +//! +//! OBS: använd bara tecken med entydig bredd i UI:t — se +//! `file_ops::tests::emoji_icons_are_wide`. + +use ratatui::style::{Color, Modifier, Style}; +use ratatui::widgets::{Block, BorderType, Borders}; + +// ─── Palett ────────────────────────────────────────────────────────────── +/// Fokusmarkering och markerade rader +pub const ACCENT: Color = Color::Cyan; +/// Rubriker och interaktiva knappar +pub const HEADER: Color = Color::Yellow; +/// Navigering ("Gå upp" m.m.) +pub const NAV: Color = Color::Magenta; +/// Positiv handling (skapa, lägg till) +pub const OK: Color = Color::Green; +/// Destruktiv handling (ta bort, stäng) +pub const DANGER: Color = Color::Red; +/// Nedtonad text: hints, avgränsare, ofokuserade ramar +pub const DIM: Color = Color::DarkGray; +/// Normal text +pub const TEXT: Color = Color::White; + +// ─── Stilar ────────────────────────────────────────────────────────────── +pub fn header() -> Style { + Style::default().fg(HEADER).add_modifier(Modifier::BOLD) +} + +pub fn nav() -> Style { + Style::default().fg(NAV).add_modifier(Modifier::BOLD) +} + +pub fn hint() -> Style { + Style::default().fg(DIM) +} + +pub fn button() -> Style { + Style::default().fg(OK).add_modifier(Modifier::BOLD) +} + +pub fn selected_row() -> Style { + Style::default() + .fg(Color::Black) + .bg(ACCENT) + .add_modifier(Modifier::BOLD) +} + +pub fn current_item() -> Style { + Style::default().fg(ACCENT).add_modifier(Modifier::BOLD) +} + +pub fn text() -> Style { + Style::default().fg(TEXT) +} + +// ─── Block/ramar ───────────────────────────────────────────────────────── +/// Standardram för en yta: rundade hörn, accentfärg vid fokus. +pub fn pane_block(title: &str, focused: bool) -> Block<'static> { + let border = if focused { + Style::default().fg(ACCENT) + } else { + Style::default().fg(DIM) + }; + let block = Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(border); + if title.is_empty() { + block + } else { + block + .title(format!(" {} ", title)) + .title_style(Style::default().fg(TEXT).add_modifier(Modifier::BOLD)) + } +} + +/// Ram för dialoger/popups: rundade hörn, färgad ram + rubrik. +pub fn dialog_block_colored(title: &str, color: Color) -> Block<'static> { + Block::default() + .title(format!(" {} ", title)) + .title_style(Style::default().fg(color).add_modifier(Modifier::BOLD)) + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(color)) +} + +/// Standarddialog: accentfärgad. +pub fn dialog_block(title: &str) -> Block<'static> { + dialog_block_colored(title, ACCENT) +} diff --git a/src/ui.rs b/src/ui.rs index 338cf9f..b74ff89 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -12,6 +12,7 @@ use ratatui::{ use unicode_width::UnicodeWidthStr; use crate::app::{App, ContextMenuItem, DialogMode, Focus}; +use crate::theme; use crate::file_ops::{self, format_size}; /// Compute the display width of a string, accounting for emojis (2 columns) and @@ -124,24 +125,15 @@ pub fn draw(f: &mut Frame, app: &mut App) { fn draw_sidebar(f: &mut Frame, app: &App, area: Rect) { let focused = app.focus == Focus::Sidebar; - let border_style = if focused { - Style::default().fg(Color::Cyan) - } else { - Style::default().fg(Color::DarkGray) - }; - - let block = Block::default() - .title(" Navigering ") - .borders(Borders::ALL) - .border_style(border_style); + let block = theme::pane_block("Navigering", focused); let inner = block.inner(area); f.render_widget(block, area); - let header_s = Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD); - let go_up_s = Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD); - let hint_s = Style::default().fg(Color::DarkGray); - let btn_s = Style::default().fg(Color::Green).add_modifier(Modifier::BOLD); + let header_s = theme::header(); + let go_up_s = theme::nav(); + let hint_s = theme::hint(); + let btn_s = theme::button(); let sep_str: String = if app.unicode_support { "\u{2500}".repeat(inner.width as usize) @@ -187,11 +179,7 @@ fn draw_sidebar(f: &mut Frame, app: &App, area: Rect) { .map(|n| n.to_string_lossy().to_string()) .unwrap_or_else(|| fav.clone()); let is_current = fav == &app.current_path.to_string_lossy().to_string(); - let style = if is_current { - Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD) - } else { - Style::default().fg(Color::White) - }; + let style = if is_current { theme::current_item() } else { theme::text() }; // ★ U+2605 is East-Asian-Width Narrow → 1 display column, safe for alignment let star = if app.unicode_support { "\u{2605}" } else { "*" }; let icon_w = display_width(star); @@ -286,11 +274,6 @@ fn draw_sidebar(f: &mut Frame, app: &App, area: Rect) { fn draw_file_view(f: &mut Frame, app: &App, area: Rect) { let focused = app.focus == Focus::FileView; - let border_style = if focused { - Style::default().fg(Color::Cyan) - } else { - Style::default().fg(Color::DarkGray) - }; let rows: Vec = app .entries @@ -327,14 +310,11 @@ fn draw_file_view(f: &mut Frame, app: &App, area: Rect) { Cell::from("Namn"), Cell::from("Storlek"), ]) - .style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD | Modifier::UNDERLINED)) + .style(theme::header().add_modifier(Modifier::UNDERLINED)) .height(1); - let path_title = format!(" {} ", app.current_path.to_string_lossy()); - let block = Block::default() - .title(path_title) - .borders(Borders::ALL) - .border_style(border_style); + let path_title = app.current_path.to_string_lossy(); + let block = theme::pane_block(&path_title, focused); let widths = [Constraint::Min(10), Constraint::Length(10)]; @@ -386,13 +366,7 @@ fn draw_file_view(f: &mut Frame, app: &App, area: Rect) { fn draw_bottom_bar(f: &mut Frame, app: &App, area: Rect) { let focused = app.focus == Focus::PathInput; - let outer_block = Block::default() - .borders(Borders::ALL) - .border_style(if focused { - Style::default().fg(Color::Cyan) - } else { - Style::default().fg(Color::DarkGray) - }); + let outer_block = theme::pane_block("", focused); let inner = outer_block.inner(area); f.render_widget(outer_block, area); @@ -544,10 +518,7 @@ fn draw_context_menu(f: &mut Frame, app: &App, menu: crate::app::ContextMenu) { let menu_area = Rect::new(x, y, menu_width, menu_height); f.render_widget(Clear, menu_area); - let block = Block::default() - .title(" Meny ") - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Cyan)); + let block = theme::dialog_block("Meny"); let inner = block.inner(menu_area); f.render_widget(block, menu_area); @@ -625,9 +596,7 @@ fn draw_context_menu(f: &mut Frame, app: &App, menu: crate::app::ContextMenu) { let sub_x = (x + menu_width).min(term_area.width.saturating_sub(sub_w)); let sub_area = Rect::new(sub_x, new_sub_row, sub_w, 4); f.render_widget(Clear, sub_area); - let sub_block = Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Green)); + let sub_block = theme::dialog_block_colored("", theme::OK); let sub_inner = sub_block.inner(sub_area); f.render_widget(sub_block, sub_area); @@ -678,11 +647,7 @@ fn draw_settings(f: &mut Frame, app: &App, right_edge: u16) { } else { " [S] Inställningar " }; - let block = Block::default() - .title(title) - .title_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Yellow)); + let block = theme::dialog_block_colored(title.trim(), theme::HEADER); let inner = block.inner(area); f.render_widget(block, area); @@ -903,10 +868,7 @@ fn draw_input_dialog(f: &mut Frame, title: &str, current: &str, area: Rect) { let dlg = Rect::new(x, y, dlg_width, dlg_height); f.render_widget(Clear, dlg); - let block = Block::default() - .title(format!(" {} ", title)) - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Cyan)); + let block = theme::dialog_block(title); let inner = block.inner(dlg); f.render_widget(block, dlg); @@ -944,10 +906,7 @@ fn draw_properties(f: &mut Frame, info: crate::app::PropertiesInfo, area: Rect) let dlg = Rect::new(x, y, dlg_width, dlg_height); f.render_widget(Clear, dlg); - let block = Block::default() - .title(" Egenskaper ") - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Cyan)); + let block = theme::dialog_block("Egenskaper"); let inner = block.inner(dlg); f.render_widget(block, dlg); @@ -1005,11 +964,7 @@ fn draw_error_dialog(f: &mut Frame, msg: &str, area: Rect) { let dlg = Rect::new(x, y, dlg_width, dlg_height); f.render_widget(Clear, dlg); - let block = Block::default() - .title(" Fel ") - .title_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)) - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Red)); + let block = theme::dialog_block_colored("Fel", theme::DANGER); let inner = block.inner(dlg); f.render_widget(block, dlg); @@ -1044,11 +999,7 @@ fn draw_confirm_dialog(f: &mut Frame, msg: &str, area: Rect) { let dlg = Rect::new(x, y, dlg_width, dlg_height); f.render_widget(Clear, dlg); - let block = Block::default() - .title(" Bekräfta ") - .title_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Yellow)); + let block = theme::dialog_block_colored("Bekräfta", theme::HEADER); let inner = block.inner(dlg); f.render_widget(block, dlg);