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 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
mod app;
|
mod app;
|
||||||
mod config;
|
mod config;
|
||||||
mod file_ops;
|
mod file_ops;
|
||||||
|
mod theme;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod events;
|
mod events;
|
||||||
|
|
||||||
|
|||||||
95
src/theme.rs
Normal file
95
src/theme.rs
Normal file
@@ -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)
|
||||||
|
}
|
||||||
85
src/ui.rs
85
src/ui.rs
@@ -12,6 +12,7 @@ use ratatui::{
|
|||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
use crate::app::{App, ContextMenuItem, DialogMode, Focus};
|
use crate::app::{App, ContextMenuItem, DialogMode, Focus};
|
||||||
|
use crate::theme;
|
||||||
use crate::file_ops::{self, format_size};
|
use crate::file_ops::{self, format_size};
|
||||||
|
|
||||||
/// Compute the display width of a string, accounting for emojis (2 columns) and
|
/// 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) {
|
fn draw_sidebar(f: &mut Frame, app: &App, area: Rect) {
|
||||||
let focused = app.focus == Focus::Sidebar;
|
let focused = app.focus == Focus::Sidebar;
|
||||||
let border_style = if focused {
|
let block = theme::pane_block("Navigering", 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 inner = block.inner(area);
|
let inner = block.inner(area);
|
||||||
f.render_widget(block, area);
|
f.render_widget(block, area);
|
||||||
|
|
||||||
let header_s = Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD);
|
let header_s = theme::header();
|
||||||
let go_up_s = Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD);
|
let go_up_s = theme::nav();
|
||||||
let hint_s = Style::default().fg(Color::DarkGray);
|
let hint_s = theme::hint();
|
||||||
let btn_s = Style::default().fg(Color::Green).add_modifier(Modifier::BOLD);
|
let btn_s = theme::button();
|
||||||
|
|
||||||
let sep_str: String = if app.unicode_support {
|
let sep_str: String = if app.unicode_support {
|
||||||
"\u{2500}".repeat(inner.width as usize)
|
"\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())
|
.map(|n| n.to_string_lossy().to_string())
|
||||||
.unwrap_or_else(|| fav.clone());
|
.unwrap_or_else(|| fav.clone());
|
||||||
let is_current = fav == &app.current_path.to_string_lossy().to_string();
|
let is_current = fav == &app.current_path.to_string_lossy().to_string();
|
||||||
let style = if is_current {
|
let style = if is_current { theme::current_item() } else { theme::text() };
|
||||||
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)
|
|
||||||
} else {
|
|
||||||
Style::default().fg(Color::White)
|
|
||||||
};
|
|
||||||
// ★ U+2605 is East-Asian-Width Narrow → 1 display column, safe for alignment
|
// ★ U+2605 is East-Asian-Width Narrow → 1 display column, safe for alignment
|
||||||
let star = if app.unicode_support { "\u{2605}" } else { "*" };
|
let star = if app.unicode_support { "\u{2605}" } else { "*" };
|
||||||
let icon_w = display_width(star);
|
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) {
|
fn draw_file_view(f: &mut Frame, app: &App, area: Rect) {
|
||||||
let focused = app.focus == Focus::FileView;
|
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<Row> = app
|
let rows: Vec<Row> = app
|
||||||
.entries
|
.entries
|
||||||
@@ -327,14 +310,11 @@ fn draw_file_view(f: &mut Frame, app: &App, area: Rect) {
|
|||||||
Cell::from("Namn"),
|
Cell::from("Namn"),
|
||||||
Cell::from("Storlek"),
|
Cell::from("Storlek"),
|
||||||
])
|
])
|
||||||
.style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD | Modifier::UNDERLINED))
|
.style(theme::header().add_modifier(Modifier::UNDERLINED))
|
||||||
.height(1);
|
.height(1);
|
||||||
|
|
||||||
let path_title = format!(" {} ", app.current_path.to_string_lossy());
|
let path_title = app.current_path.to_string_lossy();
|
||||||
let block = Block::default()
|
let block = theme::pane_block(&path_title, focused);
|
||||||
.title(path_title)
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(border_style);
|
|
||||||
|
|
||||||
let widths = [Constraint::Min(10), Constraint::Length(10)];
|
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) {
|
fn draw_bottom_bar(f: &mut Frame, app: &App, area: Rect) {
|
||||||
let focused = app.focus == Focus::PathInput;
|
let focused = app.focus == Focus::PathInput;
|
||||||
|
|
||||||
let outer_block = Block::default()
|
let outer_block = theme::pane_block("", focused);
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(if focused {
|
|
||||||
Style::default().fg(Color::Cyan)
|
|
||||||
} else {
|
|
||||||
Style::default().fg(Color::DarkGray)
|
|
||||||
});
|
|
||||||
|
|
||||||
let inner = outer_block.inner(area);
|
let inner = outer_block.inner(area);
|
||||||
f.render_widget(outer_block, 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);
|
let menu_area = Rect::new(x, y, menu_width, menu_height);
|
||||||
f.render_widget(Clear, menu_area);
|
f.render_widget(Clear, menu_area);
|
||||||
|
|
||||||
let block = Block::default()
|
let block = theme::dialog_block("Meny");
|
||||||
.title(" Meny ")
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(Style::default().fg(Color::Cyan));
|
|
||||||
|
|
||||||
let inner = block.inner(menu_area);
|
let inner = block.inner(menu_area);
|
||||||
f.render_widget(block, 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_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);
|
let sub_area = Rect::new(sub_x, new_sub_row, sub_w, 4);
|
||||||
f.render_widget(Clear, sub_area);
|
f.render_widget(Clear, sub_area);
|
||||||
let sub_block = Block::default()
|
let sub_block = theme::dialog_block_colored("", theme::OK);
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(Style::default().fg(Color::Green));
|
|
||||||
let sub_inner = sub_block.inner(sub_area);
|
let sub_inner = sub_block.inner(sub_area);
|
||||||
f.render_widget(sub_block, 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 {
|
} else {
|
||||||
" [S] Inställningar "
|
" [S] Inställningar "
|
||||||
};
|
};
|
||||||
let block = Block::default()
|
let block = theme::dialog_block_colored(title.trim(), theme::HEADER);
|
||||||
.title(title)
|
|
||||||
.title_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(Style::default().fg(Color::Yellow));
|
|
||||||
let inner = block.inner(area);
|
let inner = block.inner(area);
|
||||||
f.render_widget(block, 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);
|
let dlg = Rect::new(x, y, dlg_width, dlg_height);
|
||||||
|
|
||||||
f.render_widget(Clear, dlg);
|
f.render_widget(Clear, dlg);
|
||||||
let block = Block::default()
|
let block = theme::dialog_block(title);
|
||||||
.title(format!(" {} ", title))
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(Style::default().fg(Color::Cyan));
|
|
||||||
let inner = block.inner(dlg);
|
let inner = block.inner(dlg);
|
||||||
f.render_widget(block, 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);
|
let dlg = Rect::new(x, y, dlg_width, dlg_height);
|
||||||
|
|
||||||
f.render_widget(Clear, dlg);
|
f.render_widget(Clear, dlg);
|
||||||
let block = Block::default()
|
let block = theme::dialog_block("Egenskaper");
|
||||||
.title(" Egenskaper ")
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(Style::default().fg(Color::Cyan));
|
|
||||||
let inner = block.inner(dlg);
|
let inner = block.inner(dlg);
|
||||||
f.render_widget(block, 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);
|
let dlg = Rect::new(x, y, dlg_width, dlg_height);
|
||||||
|
|
||||||
f.render_widget(Clear, dlg);
|
f.render_widget(Clear, dlg);
|
||||||
let block = Block::default()
|
let block = theme::dialog_block_colored("Fel", theme::DANGER);
|
||||||
.title(" Fel ")
|
|
||||||
.title_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD))
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(Style::default().fg(Color::Red));
|
|
||||||
let inner = block.inner(dlg);
|
let inner = block.inner(dlg);
|
||||||
f.render_widget(block, 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);
|
let dlg = Rect::new(x, y, dlg_width, dlg_height);
|
||||||
|
|
||||||
f.render_widget(Clear, dlg);
|
f.render_widget(Clear, dlg);
|
||||||
let block = Block::default()
|
let block = theme::dialog_block_colored("Bekräfta", theme::HEADER);
|
||||||
.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 inner = block.inner(dlg);
|
let inner = block.inner(dlg);
|
||||||
f.render_widget(block, dlg);
|
f.render_widget(block, dlg);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user