Introduce central theme module; rounded borders and consistent styling

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 09:39:39 +02:00
parent 6aee7ce00a
commit 9cebeae716
3 changed files with 95 additions and 32 deletions

View File

@@ -7,6 +7,7 @@ mod log;
mod pty;
mod render;
mod server;
mod theme;
use app::{App, AppIpcOut, WindowContent};
use config::Config;

View File

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

70
src/theme.rs Normal file
View File

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