From 9cebeae7168093ee3837cce1df085a3dcaf10b05 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 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All colors/styles for windows, panel, dropdown and desktop now live in src/theme.rs — restyle the whole WM from one file. Windows and menus use rounded borders; hover/focus states share one accent style. Co-Authored-By: Claude Fable 5 --- src/main.rs | 1 + src/render.rs | 56 ++++++++++++++++++----------------------- src/theme.rs | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 32 deletions(-) create mode 100644 src/theme.rs diff --git a/src/main.rs b/src/main.rs index 0cccee5..39701c3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ mod log; mod pty; mod render; mod server; +mod theme; use app::{App, AppIpcOut, WindowContent}; use config::Config; diff --git a/src/render.rs b/src/render.rs index e368d64..6e26f8a 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1,10 +1,11 @@ use crate::app::{App, DropdownState, FloatingWindow, ResizeEdge, WindowContent}; +use crate::theme; use crate::config::StatusAlign; use ratatui::{ layout::Rect, style::{Color, Modifier, Style}, text::{Line, Span}, - widgets::{Block, Borders, Clear, Paragraph, Wrap}, + widgets::{Block, BorderType, Borders, Clear, Paragraph, Wrap}, Frame, }; @@ -15,7 +16,7 @@ pub fn render(frame: &mut Frame, app: &App) { let bg_color = if app.config.background_image.is_some() { Color::Reset } else { - Color::Indexed(235) + theme::DESKTOP_BG }; frame.render_widget( Block::default().style(Style::default().bg(bg_color)), @@ -76,15 +77,15 @@ fn render_panel( ) { // Panelens bakgrund frame.render_widget( - Block::default().style(Style::default().bg(Color::Indexed(238))), + Block::default().style(Style::default().bg(theme::PANEL_BG)), rect, ); // Logotyp – klickbar meny-knapp med hover-highlight let logo_style = if hovered_tui { - Style::default().fg(Color::Black).bg(Color::Cyan).add_modifier(Modifier::BOLD) + theme::hover_highlight() } else { - Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD) + theme::logo() }; frame.render_widget( Paragraph::new(Span::styled(" TUI-WM ", logo_style)), @@ -92,9 +93,9 @@ fn render_panel( ); // Fokus-indikator: grön ● = skrivbord fokuserat, grå ● = fönster fokuserat - let dot_color = if desktop_focused { Color::Green } else { Color::Indexed(240) }; + let dot_color = if desktop_focused { theme::OK } else { theme::HINT }; frame.render_widget( - Paragraph::new(Span::styled("●", Style::default().fg(dot_color).bg(Color::Indexed(238)))), + Paragraph::new(Span::styled("●", Style::default().fg(dot_color).bg(theme::PANEL_BG))), Rect::new(rect.x + 10, rect.y, 1, 1), ); @@ -103,9 +104,9 @@ fn render_panel( let Some(&ir) = item_rects.get(ii) else { continue }; let is_hovered = hovered == Some((panel_idx, ii)); let style = if is_hovered { - Style::default().fg(Color::Black).bg(Color::Cyan).add_modifier(Modifier::BOLD) + theme::hover_highlight() } else { - Style::default().fg(Color::White).bg(Color::Indexed(238)) + Style::default().fg(theme::TEXT).bg(theme::PANEL_BG) }; frame.render_widget( Paragraph::new(Span::styled(format!(" {} ", item.label), style)), @@ -131,7 +132,7 @@ fn render_panel( frame.render_widget( Paragraph::new(Span::styled( padded, - Style::default().fg(Color::Indexed(252)).bg(Color::Indexed(238)), + theme::panel_item(), )), draw_rect, ); @@ -150,7 +151,7 @@ fn render_panel( frame.render_widget( Paragraph::new(Span::styled( padded, - Style::default().fg(Color::Indexed(252)).bg(Color::Indexed(238)), + theme::panel_item(), )), draw_rect, ); @@ -164,16 +165,17 @@ fn render_dropdown(frame: &mut Frame, dd: &DropdownState) { frame.render_widget( Block::default() .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Cyan)) - .style(Style::default().bg(Color::Indexed(236))), + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(theme::ACCENT)) + .style(Style::default().bg(theme::MENU_BG)), border_rect, ); for (i, (item, &ir)) in dd.items.iter().zip(dd.item_rects.iter()).enumerate() { let is_hovered = dd.hovered == Some(i); let style = if is_hovered { - Style::default().fg(Color::Black).bg(Color::Cyan).add_modifier(Modifier::BOLD) + theme::hover_highlight() } else { - Style::default().fg(Color::White).bg(Color::Indexed(236)) + Style::default().fg(theme::TEXT).bg(theme::MENU_BG) }; // ir pekar redan på den faktiska raden innanför kanten let draw_rect = Rect::new(ir.x + 1, ir.y, ir.width.saturating_sub(2), 1); @@ -200,15 +202,7 @@ fn render_window( let is_focused = focused_id == Some(window.id); let is_resizing = window.resizing.is_some(); - let border_style = if is_resizing { - Style::default().fg(Color::Yellow) - } else if resize_hover.is_some() { - Style::default().fg(Color::Indexed(220)) // ljusgul vid hover på kant - } else if is_focused { - Style::default().fg(Color::Cyan) - } else { - Style::default().fg(Color::Indexed(242)) - }; + let border_style = theme::window_border(is_focused, is_resizing, resize_hover.is_some()); let title_text = match &window.content { WindowContent::Terminal { title, .. } => format!(" {} ", title), @@ -219,11 +213,9 @@ fn render_window( frame.render_widget(Clear, full_rect); frame.render_widget( Block::default() - .title(Span::styled( - &title_text, - Style::default().fg(Color::White).add_modifier(Modifier::BOLD), - )) + .title(Span::styled(&title_text, theme::window_title())) .borders(Borders::ALL) + .border_type(BorderType::Rounded) .border_style(border_style) .style(Style::default().bg(match &window.content { WindowContent::Terminal { .. } => terminal_bg, @@ -237,9 +229,9 @@ fn render_window( if cb.x + 3 <= full_rect.x + full_rect.width { let close_hov = hovered_close == Some(window.id); let close_style = if close_hov { - Style::default().fg(Color::White).bg(Color::Red).add_modifier(Modifier::BOLD) + Style::default().fg(theme::TEXT).bg(theme::DANGER).add_modifier(Modifier::BOLD) } else { - Style::default().fg(Color::Red) + Style::default().fg(theme::DANGER) }; frame.render_widget(Paragraph::new(Span::styled("[X]", close_style)), cb); } @@ -326,9 +318,9 @@ fn render_popup_dialog( for (i, label) in labels.iter().enumerate() { let w = label.len() as u16; let style = if i == *selected { - Style::default().fg(Color::Black).bg(Color::Cyan).add_modifier(Modifier::BOLD) + theme::hover_highlight() } else { - Style::default().fg(Color::White).bg(Color::Indexed(238)) + Style::default().fg(theme::TEXT).bg(theme::PANEL_BG) }; if btn_x + w <= rect.x + rect.width { frame.render_widget( diff --git a/src/theme.rs b/src/theme.rs new file mode 100644 index 0000000..3538637 --- /dev/null +++ b/src/theme.rs @@ -0,0 +1,70 @@ +//! Centralt tema för TUI-WM. +//! +//! ALLA färger och återkommande stilar definieras här — ändra utseendet +//! på hela fönsterhanteraren från en enda fil. render.rs ska inte +//! hårdkoda `Color::`-literaler för sådant som temat täcker. + +use ratatui::style::{Color, Modifier, Style}; + +// ─── Palett ────────────────────────────────────────────────────────────── +/// Fokusmarkering (fönsterram, logga, markerade menyrader) +pub const ACCENT: Color = Color::Cyan; +/// Pågående storleksändring +pub const RESIZE: Color = Color::Yellow; +/// Hover över fönsterkant (inför resize) +pub const RESIZE_HOVER: Color = Color::Indexed(220); +/// Ofokuserad fönsterram +pub const BORDER_DIM: Color = Color::Indexed(242); +/// Skrivbordsbakgrund (när ingen bakgrundsbild används) +pub const DESKTOP_BG: Color = Color::Indexed(235); +/// Panelens bakgrund +pub const PANEL_BG: Color = Color::Indexed(238); +/// Dropdown-menyns bakgrund +pub const MENU_BG: Color = Color::Indexed(236); +/// Normal text +pub const TEXT: Color = Color::White; +/// Panel-text (något nedtonad) +pub const PANEL_TEXT: Color = Color::Indexed(252); +/// Hints och inaktiva indikatorer +pub const HINT: Color = Color::Indexed(240); +/// Positiv indikator (skrivbordsfokus-punkten) +pub const OK: Color = Color::Green; +/// Destruktivt (stäng-knappen) +pub const DANGER: Color = Color::Red; + +// ─── Stilar ────────────────────────────────────────────────────────────── +/// Ramstil för ett fönster utifrån dess tillstånd. +pub fn window_border(focused: bool, resizing: bool, resize_hover: bool) -> Style { + if resizing { + Style::default().fg(RESIZE) + } else if resize_hover { + Style::default().fg(RESIZE_HOVER) + } else if focused { + Style::default().fg(ACCENT) + } else { + Style::default().fg(BORDER_DIM) + } +} + +/// Markerad rad/knapp (hover): inverterad accent. +pub fn hover_highlight() -> Style { + Style::default() + .fg(Color::Black) + .bg(ACCENT) + .add_modifier(Modifier::BOLD) +} + +/// TUI-WM-loggan i panelen. +pub fn logo() -> Style { + Style::default().fg(ACCENT).add_modifier(Modifier::BOLD) +} + +/// Panel-objekt (menyknappar) i normalläge. +pub fn panel_item() -> Style { + Style::default().fg(PANEL_TEXT).bg(PANEL_BG) +} + +/// Fönstertitel. +pub fn window_title() -> Style { + Style::default().fg(TEXT).add_modifier(Modifier::BOLD) +}