feat: desktop visual refresh + 12-theme pack
- 12 new themes (neon/goth/dev/cozy) + hand-tuned Dark/Light (19 total) - theme-aware semantic accents replacing hardcoded selection rings - List/Card/Grid video-list modes with global default + per-view override - unified placeholder thumbnails + typography/spacing refresh
This commit is contained in:
commit
cbade2cfd7
3 changed files with 1035 additions and 206 deletions
662
src/app.rs
662
src/app.rs
|
|
@ -76,7 +76,24 @@ enum SortMode {
|
||||||
ChannelAsc,
|
ChannelAsc,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
enum ViewMode {
|
||||||
|
List,
|
||||||
|
Card,
|
||||||
|
Grid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViewMode {
|
||||||
|
fn from_config(s: &str) -> Self {
|
||||||
|
match s {
|
||||||
|
"card" => ViewMode::Card,
|
||||||
|
"grid" => ViewMode::Grid,
|
||||||
|
_ => ViewMode::List,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||||
enum SidebarView {
|
enum SidebarView {
|
||||||
Channels,
|
Channels,
|
||||||
All,
|
All,
|
||||||
|
|
@ -165,6 +182,12 @@ pub struct App {
|
||||||
plex_status: String,
|
plex_status: String,
|
||||||
db: Database,
|
db: Database,
|
||||||
card_density: f32,
|
card_density: f32,
|
||||||
|
/// Theme-aware accent colors, recomputed whenever the theme changes.
|
||||||
|
theme_accents: crate::theme::ThemeAccents,
|
||||||
|
/// Global default video-list render mode (from config).
|
||||||
|
default_view_mode: ViewMode,
|
||||||
|
/// Per-SidebarView overrides; a view absent here falls back to default.
|
||||||
|
view_mode_overrides: std::collections::HashMap<SidebarView, ViewMode>,
|
||||||
/// When set, the global UI zoom changed and config should be saved at
|
/// When set, the global UI zoom changed and config should be saved at
|
||||||
/// this instant (debounced so a slider drag / key-repeat doesn't write
|
/// this instant (debounced so a slider drag / key-repeat doesn't write
|
||||||
/// config.toml every frame). Synced from `ctx.zoom_factor()` in update().
|
/// config.toml every frame). Synced from `ctx.zoom_factor()` in update().
|
||||||
|
|
@ -356,6 +379,43 @@ fn idx_to_sponsorblock(i: usize) -> Option<String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
/// The view mode to use for `view`: the per-view override if set, else
|
||||||
|
/// the global default.
|
||||||
|
fn view_mode_for(&self, view: &SidebarView) -> ViewMode {
|
||||||
|
self.view_mode_overrides
|
||||||
|
.get(view)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(self.default_view_mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Paint a consistent missing-thumbnail placeholder inside `rect`:
|
||||||
|
/// a theme-tinted two-tone fill + a single glyph. Used for both channel
|
||||||
|
/// and video cards so the empty states stop diverging.
|
||||||
|
fn paint_thumb_placeholder(&self, ui: &egui::Ui, rect: egui::Rect, glyph: &str, density: f32) {
|
||||||
|
let v = ui.visuals();
|
||||||
|
let top = v.faint_bg_color.to_array();
|
||||||
|
let bot = v.panel_fill.to_array();
|
||||||
|
// egui has no native gradient; fake one with two stacked rects.
|
||||||
|
let mid = rect.top() + rect.height() * 0.5;
|
||||||
|
ui.painter().rect_filled(
|
||||||
|
egui::Rect::from_min_max(rect.min, egui::pos2(rect.max.x, mid)),
|
||||||
|
4.0,
|
||||||
|
egui::Color32::from_rgba_unmultiplied(top[0], top[1], top[2], 255),
|
||||||
|
);
|
||||||
|
ui.painter().rect_filled(
|
||||||
|
egui::Rect::from_min_max(egui::pos2(rect.min.x, mid), rect.max),
|
||||||
|
4.0,
|
||||||
|
egui::Color32::from_rgba_unmultiplied(bot[0], bot[1], bot[2], 255),
|
||||||
|
);
|
||||||
|
ui.painter().text(
|
||||||
|
rect.center(),
|
||||||
|
egui::Align2::CENTER_CENTER,
|
||||||
|
glyph,
|
||||||
|
egui::FontId::proportional(24.0 * density),
|
||||||
|
v.weak_text_color(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new(cc: &eframe::CreationContext<'_>, tray: Option<crate::tray::TrayHandle>) -> Self {
|
pub fn new(cc: &eframe::CreationContext<'_>, tray: Option<crate::tray::TrayHandle>) -> Self {
|
||||||
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||||
let config_path = cwd.join("config.toml");
|
let config_path = cwd.join("config.toml");
|
||||||
|
|
@ -369,6 +429,14 @@ impl App {
|
||||||
};
|
};
|
||||||
|
|
||||||
theme::apply(&cc.egui_ctx, &config.ui.theme);
|
theme::apply(&cc.egui_ctx, &config.ui.theme);
|
||||||
|
let theme_accents = theme::accents_for(&config.ui.theme);
|
||||||
|
let default_view_mode = ViewMode::from_config(&config.ui.default_view_mode);
|
||||||
|
// Typography & spacing baseline: slightly larger base text, more
|
||||||
|
// breathable item spacing, comfortable button padding.
|
||||||
|
let mut style = (*cc.egui_ctx.style()).clone();
|
||||||
|
style.spacing.item_spacing = egui::vec2(8.0, 5.0);
|
||||||
|
style.spacing.button_padding = egui::vec2(6.0, 3.0);
|
||||||
|
cc.egui_ctx.set_style(style);
|
||||||
// Restore the persisted global UI zoom. egui's built-in Ctrl +/-/0
|
// Restore the persisted global UI zoom. egui's built-in Ctrl +/-/0
|
||||||
// also drives zoom_factor; update() keeps config.ui.ui_scale synced
|
// also drives zoom_factor; update() keeps config.ui.ui_scale synced
|
||||||
// and persists changes so the size survives restarts.
|
// and persists changes so the size survives restarts.
|
||||||
|
|
@ -551,6 +619,9 @@ impl App {
|
||||||
plex_status: String::new(),
|
plex_status: String::new(),
|
||||||
db,
|
db,
|
||||||
card_density: 1.0,
|
card_density: 1.0,
|
||||||
|
theme_accents,
|
||||||
|
default_view_mode,
|
||||||
|
view_mode_overrides: Default::default(),
|
||||||
scale_save_at: None,
|
scale_save_at: None,
|
||||||
sort_mode: SortMode::Title,
|
sort_mode: SortMode::Title,
|
||||||
watched,
|
watched,
|
||||||
|
|
@ -3176,6 +3247,36 @@ impl App {
|
||||||
{
|
{
|
||||||
self.config.ui.theme = id.to_string();
|
self.config.ui.theme = id.to_string();
|
||||||
theme::apply(ctx, id);
|
theme::apply(ctx, id);
|
||||||
|
self.theme_accents = theme::accents_for(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.label("Default view:");
|
||||||
|
egui::ComboBox::from_id_salt("default_view_mode_combo")
|
||||||
|
.selected_text(match self.default_view_mode {
|
||||||
|
ViewMode::List => "List",
|
||||||
|
ViewMode::Card => "Card",
|
||||||
|
ViewMode::Grid => "Grid",
|
||||||
|
})
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
for (mode, label) in [
|
||||||
|
(ViewMode::List, "List"),
|
||||||
|
(ViewMode::Card, "Card"),
|
||||||
|
(ViewMode::Grid, "Grid"),
|
||||||
|
] {
|
||||||
|
if ui
|
||||||
|
.selectable_label(self.default_view_mode == mode, label)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
self.default_view_mode = mode;
|
||||||
|
self.config.ui.default_view_mode = match mode {
|
||||||
|
ViewMode::List => "list",
|
||||||
|
ViewMode::Card => "card",
|
||||||
|
ViewMode::Grid => "grid",
|
||||||
|
}
|
||||||
|
.to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -3969,14 +4070,7 @@ impl App {
|
||||||
.paint_at(ui, thumb_rect);
|
.paint_at(ui, thumb_rect);
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
ui.painter().rect_filled(thumb_rect, 4.0, egui::Color32::from_gray(30));
|
self.paint_thumb_placeholder(ui, thumb_rect, "🎬", density);
|
||||||
ui.painter().text(
|
|
||||||
thumb_rect.center(),
|
|
||||||
egui::Align2::CENTER_CENTER,
|
|
||||||
"📺",
|
|
||||||
egui::FontId::proportional(28.0 * density),
|
|
||||||
egui::Color32::from_gray(100),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4180,209 +4274,369 @@ impl App {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// View-mode toggle: ☰ List / ▢ Card / ◫ Grid. Writes a per-view
|
||||||
|
// override; a view with no override falls back to the global default.
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label(egui::RichText::new("View:").weak().small());
|
||||||
|
let current = self.view_mode_for(&self.sidebar_view);
|
||||||
|
for (mode, label) in [
|
||||||
|
(ViewMode::List, "☰ List"),
|
||||||
|
(ViewMode::Card, "▢ Card"),
|
||||||
|
(ViewMode::Grid, "◫ Grid"),
|
||||||
|
] {
|
||||||
|
if ui.selectable_label(current == mode, label).clicked() {
|
||||||
|
self.view_mode_overrides
|
||||||
|
.insert(self.sidebar_view.clone(), mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
let density = self.card_density;
|
let density = self.card_density;
|
||||||
|
let view_mode = self.view_mode_for(&self.sidebar_view);
|
||||||
egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
|
egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
|
||||||
let thumb_w = (176.0 * density).round();
|
match view_mode {
|
||||||
let thumb_h = (99.0 * density).round();
|
ViewMode::List | ViewMode::Card => {
|
||||||
let thumb_size = egui::vec2(thumb_w, thumb_h);
|
self.render_video_rows(ui, ctx, &cards, density, show_channel, view_mode == ViewMode::Card);
|
||||||
|
|
||||||
for card in &cards {
|
|
||||||
let selected = self.selected_video.as_deref() == Some(card.id.as_str());
|
|
||||||
let is_playing = self.currently_playing.as_deref() == Some(card.id.as_str());
|
|
||||||
let bulk_checked = self.bulk_selected.contains(&card.id);
|
|
||||||
let mut clicked_card = false;
|
|
||||||
let mut play_card = false;
|
|
||||||
let mut toggle_watched_card = false;
|
|
||||||
// Flag toggles deferred outside the row closure so we can
|
|
||||||
// mutate `self.flags` + DB without fighting the borrow checker.
|
|
||||||
let mut toggle_flag_card: Option<&'static str> = None;
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
let (rect, resp) = ui.allocate_exact_size(thumb_size, egui::Sense::click());
|
|
||||||
let texture = card.thumb_path.as_ref().and_then(|p| self.texture(ctx, p));
|
|
||||||
match &texture {
|
|
||||||
Some(handle) => {
|
|
||||||
egui::Image::new(handle)
|
|
||||||
.maintain_aspect_ratio(true)
|
|
||||||
.paint_at(ui, rect);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
ui.painter().rect_filled(rect, 4.0, egui::Color32::from_gray(38));
|
|
||||||
ui.painter().text(
|
|
||||||
rect.center(),
|
|
||||||
egui::Align2::CENTER_CENTER,
|
|
||||||
"▶",
|
|
||||||
egui::FontId::proportional(26.0 * density),
|
|
||||||
egui::Color32::from_gray(110),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if selected {
|
|
||||||
ui.painter().rect_stroke(
|
|
||||||
rect,
|
|
||||||
4.0,
|
|
||||||
egui::Stroke::new(2.0, egui::Color32::from_rgb(120, 170, 230)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if is_playing {
|
|
||||||
ui.painter().rect_stroke(
|
|
||||||
rect,
|
|
||||||
4.0,
|
|
||||||
egui::Stroke::new(2.0, egui::Color32::from_rgb(110, 200, 110)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if bulk_checked {
|
|
||||||
ui.painter().rect_stroke(
|
|
||||||
rect,
|
|
||||||
4.0,
|
|
||||||
egui::Stroke::new(3.0, egui::Color32::from_rgb(180, 130, 240)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if card.watched {
|
|
||||||
ui.painter().rect_filled(
|
|
||||||
egui::Rect::from_min_size(
|
|
||||||
rect.min,
|
|
||||||
egui::vec2(rect.width(), rect.height() * 0.18),
|
|
||||||
),
|
|
||||||
0.0,
|
|
||||||
egui::Color32::from_rgba_premultiplied(30, 140, 60, 200),
|
|
||||||
);
|
|
||||||
ui.painter().text(
|
|
||||||
rect.min + egui::vec2(4.0, 2.0),
|
|
||||||
egui::Align2::LEFT_TOP,
|
|
||||||
"✓ watched",
|
|
||||||
egui::FontId::proportional(10.0 * density),
|
|
||||||
egui::Color32::WHITE,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.clicked() {
|
|
||||||
clicked_card = true;
|
|
||||||
}
|
|
||||||
if resp.double_clicked() {
|
|
||||||
play_card = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
let title_color = if card.watched {
|
|
||||||
ui.visuals().weak_text_color()
|
|
||||||
} else {
|
|
||||||
ui.visuals().text_color()
|
|
||||||
};
|
|
||||||
if ui
|
|
||||||
.selectable_label(
|
|
||||||
selected,
|
|
||||||
egui::RichText::new(&card.title)
|
|
||||||
.strong()
|
|
||||||
.color(title_color),
|
|
||||||
)
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
clicked_card = true;
|
|
||||||
}
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
if show_channel {
|
|
||||||
ui.label(egui::RichText::new(&card.channel_name).small().weak());
|
|
||||||
ui.label(egui::RichText::new("·").weak());
|
|
||||||
}
|
|
||||||
ui.label(egui::RichText::new(&card.id).small().monospace().weak());
|
|
||||||
if let Some(date) = card.upload_date.as_deref().map(format_upload_date) {
|
|
||||||
if !date.is_empty() {
|
|
||||||
ui.label(egui::RichText::new("·").weak());
|
|
||||||
ui.label(egui::RichText::new(date).small().weak());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(secs) = card.duration_secs {
|
|
||||||
ui.label(egui::RichText::new("·").weak());
|
|
||||||
ui.label(egui::RichText::new(format_duration(secs)).small().weak());
|
|
||||||
}
|
|
||||||
if let Some(bytes) = card.file_size {
|
|
||||||
ui.label(egui::RichText::new("·").weak());
|
|
||||||
ui.label(egui::RichText::new(format_size(bytes)).small().weak());
|
|
||||||
}
|
|
||||||
if card.has_live_chat {
|
|
||||||
ui.label(egui::RichText::new("· 💬").small().weak());
|
|
||||||
}
|
|
||||||
if card.video_path.is_none() {
|
|
||||||
ui.label(
|
|
||||||
egui::RichText::new("· no video file")
|
|
||||||
.small()
|
|
||||||
.color(egui::Color32::from_rgb(200, 140, 90)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
if self.bulk_mode {
|
|
||||||
let chk_label = if bulk_checked { "☑" } else { "☐" };
|
|
||||||
if ui.small_button(chk_label).clicked() {
|
|
||||||
clicked_card = true; // handled below
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if card.video_path.is_some() && ui.small_button("▶ Play").clicked() {
|
|
||||||
play_card = true;
|
|
||||||
}
|
|
||||||
if let Some(pos) = card.resume_pos {
|
|
||||||
if pos > 5.0 && ui.small_button(format!("⏩ {}", format_duration(pos))).clicked() {
|
|
||||||
play_card = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ui.small_button("Details").clicked() {
|
|
||||||
clicked_card = true;
|
|
||||||
}
|
|
||||||
let w_label = if card.watched { "✓" } else { "○" };
|
|
||||||
if ui.small_button(w_label).on_hover_text("Toggle watched").clicked() {
|
|
||||||
toggle_watched_card = true;
|
|
||||||
}
|
|
||||||
let fav_label = if card.favourite { "★" } else { "☆" };
|
|
||||||
if ui.small_button(fav_label).on_hover_text("Toggle favourite").clicked() {
|
|
||||||
toggle_flag_card = Some("favourite");
|
|
||||||
}
|
|
||||||
let bm_label = if card.bookmark { "🔖" } else { "🔖̲" };
|
|
||||||
if ui.small_button(bm_label).on_hover_text("Toggle bookmark").clicked() {
|
|
||||||
toggle_flag_card = Some("bookmark");
|
|
||||||
}
|
|
||||||
let wait_label = if card.waiting { "⏳" } else { "⏰" };
|
|
||||||
if ui.small_button(wait_label).on_hover_text("Toggle waiting").clicked() {
|
|
||||||
toggle_flag_card = Some("waiting");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if self.bulk_mode {
|
|
||||||
if clicked_card {
|
|
||||||
let id = card.id.clone();
|
|
||||||
if self.bulk_selected.contains(&id) {
|
|
||||||
self.bulk_selected.remove(&id);
|
|
||||||
} else {
|
|
||||||
self.bulk_selected.insert(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if play_card {
|
|
||||||
if let Some(p) = card.video_path.clone() {
|
|
||||||
let id = card.id.clone();
|
|
||||||
self.play_with_tracking(&p, id);
|
|
||||||
}
|
|
||||||
self.selected_video = Some(card.id.clone());
|
|
||||||
} else if clicked_card {
|
|
||||||
self.selected_video = Some(card.id.clone());
|
|
||||||
}
|
}
|
||||||
if toggle_watched_card {
|
ViewMode::Grid => {
|
||||||
let id = card.id.clone();
|
self.render_video_grid(ui, ctx, &cards, density, show_channel);
|
||||||
self.toggle_watched(&id);
|
|
||||||
}
|
}
|
||||||
if let Some(flag) = toggle_flag_card {
|
|
||||||
let id = card.id.clone();
|
|
||||||
self.toggle_video_flag(&id, flag);
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
self.cards_cache = cards;
|
self.cards_cache = cards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Row-based render path shared by List and Card modes. When `as_card`
|
||||||
|
/// is true, each row is wrapped in a rounded faint-bg card with a hover
|
||||||
|
/// accent ring; otherwise the row is rendered flat (legacy List style).
|
||||||
|
fn render_video_rows(
|
||||||
|
&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
ctx: &egui::Context,
|
||||||
|
cards: &[Card],
|
||||||
|
density: f32,
|
||||||
|
show_channel: bool,
|
||||||
|
as_card: bool,
|
||||||
|
) {
|
||||||
|
let thumb_w = (176.0 * density).round();
|
||||||
|
let thumb_h = (99.0 * density).round();
|
||||||
|
let thumb_size = egui::vec2(thumb_w, thumb_h);
|
||||||
|
|
||||||
|
for card in cards {
|
||||||
|
let selected = self.selected_video.as_deref() == Some(card.id.as_str());
|
||||||
|
let is_playing = self.currently_playing.as_deref() == Some(card.id.as_str());
|
||||||
|
let bulk_checked = self.bulk_selected.contains(&card.id);
|
||||||
|
let mut clicked_card = false;
|
||||||
|
let mut play_card = false;
|
||||||
|
let mut toggle_watched_card = false;
|
||||||
|
// Flag toggles deferred outside the row closure so we can
|
||||||
|
// mutate `self.flags` + DB without fighting the borrow checker.
|
||||||
|
let mut toggle_flag_card: Option<&'static str> = None;
|
||||||
|
|
||||||
|
// Wrap each row in a Frame when rendering as a Card.
|
||||||
|
let mut card_rect: Option<egui::Rect> = None;
|
||||||
|
if as_card {
|
||||||
|
let frame = egui::Frame::default()
|
||||||
|
.fill(ui.visuals().faint_bg_color)
|
||||||
|
.stroke(egui::Stroke::new(
|
||||||
|
1.0,
|
||||||
|
ui.visuals().widgets.noninteractive.bg_stroke.color,
|
||||||
|
))
|
||||||
|
.rounding(egui::Rounding::same(8.0))
|
||||||
|
.inner_margin(egui::Margin::same(8.0))
|
||||||
|
.outer_margin(egui::Margin::symmetric(0.0, 3.0));
|
||||||
|
let resp = frame.show(ui, |ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
self.render_row_body(
|
||||||
|
ui, ctx, card, density, thumb_size, show_channel,
|
||||||
|
selected, is_playing, bulk_checked,
|
||||||
|
&mut clicked_card, &mut play_card,
|
||||||
|
&mut toggle_watched_card, &mut toggle_flag_card,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}).response;
|
||||||
|
if resp.hovered() {
|
||||||
|
card_rect = Some(resp.rect);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
self.render_row_body(
|
||||||
|
ui, ctx, card, density, thumb_size, show_channel,
|
||||||
|
selected, is_playing, bulk_checked,
|
||||||
|
&mut clicked_card, &mut play_card,
|
||||||
|
&mut toggle_watched_card, &mut toggle_flag_card,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(rect) = card_rect {
|
||||||
|
ui.painter().rect_stroke(
|
||||||
|
rect,
|
||||||
|
8.0,
|
||||||
|
egui::Stroke::new(1.5, self.theme_accents.accent),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.apply_card_actions(
|
||||||
|
card, clicked_card, play_card, toggle_watched_card, toggle_flag_card,
|
||||||
|
);
|
||||||
|
ui.separator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Grid mode: YouTube/Plex-style vertical cards in a responsive grid.
|
||||||
|
fn render_video_grid(
|
||||||
|
&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
ctx: &egui::Context,
|
||||||
|
cards: &[Card],
|
||||||
|
density: f32,
|
||||||
|
show_channel: bool,
|
||||||
|
) {
|
||||||
|
let thumb_w = (176.0 * density).round();
|
||||||
|
let thumb_h = (99.0 * density).round();
|
||||||
|
let card_w = thumb_w + 8.0;
|
||||||
|
let avail = ui.available_width();
|
||||||
|
let cols = ((avail / card_w).floor() as usize).max(1);
|
||||||
|
|
||||||
|
// Grid mode renders one cell at a time but still uses the deferred-
|
||||||
|
// flags pattern: actions are applied per-card right after its cell.
|
||||||
|
egui::Grid::new("video_grid")
|
||||||
|
.num_columns(cols)
|
||||||
|
.spacing([8.0, 8.0])
|
||||||
|
.show(ui, |ui| {
|
||||||
|
for card in cards {
|
||||||
|
let selected = self.selected_video.as_deref() == Some(card.id.as_str());
|
||||||
|
let is_playing = self.currently_playing.as_deref() == Some(card.id.as_str());
|
||||||
|
let bulk_checked = self.bulk_selected.contains(&card.id);
|
||||||
|
let mut clicked_card = false;
|
||||||
|
let mut play_card = false;
|
||||||
|
let mut toggle_watched_card = false;
|
||||||
|
let mut toggle_flag_card: Option<&'static str> = None;
|
||||||
|
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
// `render_row_body` owns the thumbnail + rings + clicks,
|
||||||
|
// so the grid just lays out title/meta below it.
|
||||||
|
self.render_row_body(
|
||||||
|
ui, ctx, card, density, egui::vec2(thumb_w, thumb_h), show_channel,
|
||||||
|
selected, is_playing, bulk_checked,
|
||||||
|
&mut clicked_card, &mut play_card,
|
||||||
|
&mut toggle_watched_card, &mut toggle_flag_card,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.apply_card_actions(
|
||||||
|
card, clicked_card, play_card, toggle_watched_card, toggle_flag_card,
|
||||||
|
);
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The thumbnail + metadata + flag-button body shared by List/Card/Grid.
|
||||||
|
/// Owns the thumbnail paint, selection/playing/bulk rings, watched
|
||||||
|
/// banner, and click/double-click sensing. Callers lay out the cell
|
||||||
|
/// (horizontal for List/Card, vertical for Grid) and pass `thumb_size`.
|
||||||
|
fn render_row_body(
|
||||||
|
&mut self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
ctx: &egui::Context,
|
||||||
|
card: &Card,
|
||||||
|
density: f32,
|
||||||
|
thumb_size: egui::Vec2,
|
||||||
|
show_channel: bool,
|
||||||
|
selected: bool,
|
||||||
|
is_playing: bool,
|
||||||
|
bulk_checked: bool,
|
||||||
|
clicked_card: &mut bool,
|
||||||
|
play_card: &mut bool,
|
||||||
|
toggle_watched_card: &mut bool,
|
||||||
|
toggle_flag_card: &mut Option<&'static str>,
|
||||||
|
) {
|
||||||
|
let (rect, resp) = ui.allocate_exact_size(thumb_size, egui::Sense::click());
|
||||||
|
let texture = card.thumb_path.as_ref().and_then(|p| self.texture(ctx, p));
|
||||||
|
match &texture {
|
||||||
|
Some(handle) => {
|
||||||
|
egui::Image::new(handle)
|
||||||
|
.maintain_aspect_ratio(true)
|
||||||
|
.paint_at(ui, rect);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.paint_thumb_placeholder(ui, rect, "🎬", density);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if selected {
|
||||||
|
ui.painter().rect_stroke(
|
||||||
|
rect,
|
||||||
|
4.0,
|
||||||
|
egui::Stroke::new(2.0, self.theme_accents.accent),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if is_playing {
|
||||||
|
ui.painter().rect_stroke(
|
||||||
|
rect,
|
||||||
|
4.0,
|
||||||
|
egui::Stroke::new(2.0, self.theme_accents.success),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if bulk_checked {
|
||||||
|
ui.painter().rect_stroke(
|
||||||
|
rect,
|
||||||
|
4.0,
|
||||||
|
egui::Stroke::new(3.0, self.theme_accents.warning),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if card.watched {
|
||||||
|
ui.painter().rect_filled(
|
||||||
|
egui::Rect::from_min_size(
|
||||||
|
rect.min,
|
||||||
|
egui::vec2(rect.width(), rect.height() * 0.18),
|
||||||
|
),
|
||||||
|
0.0,
|
||||||
|
egui::Color32::from_rgba_premultiplied(30, 140, 60, 200),
|
||||||
|
);
|
||||||
|
ui.painter().text(
|
||||||
|
rect.min + egui::vec2(4.0, 2.0),
|
||||||
|
egui::Align2::LEFT_TOP,
|
||||||
|
"✓ watched",
|
||||||
|
egui::FontId::proportional(10.0 * density),
|
||||||
|
egui::Color32::WHITE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.clicked() {
|
||||||
|
*clicked_card = true;
|
||||||
|
}
|
||||||
|
if resp.double_clicked() {
|
||||||
|
*play_card = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
let title_color = if card.watched {
|
||||||
|
ui.visuals().weak_text_color()
|
||||||
|
} else {
|
||||||
|
ui.visuals().text_color()
|
||||||
|
};
|
||||||
|
if ui
|
||||||
|
.selectable_label(
|
||||||
|
selected,
|
||||||
|
egui::RichText::new(&card.title)
|
||||||
|
.strong()
|
||||||
|
.size(14.0)
|
||||||
|
.color(title_color),
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
*clicked_card = true;
|
||||||
|
}
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if show_channel {
|
||||||
|
ui.label(egui::RichText::new(&card.channel_name).small().weak());
|
||||||
|
ui.label(egui::RichText::new("·").weak());
|
||||||
|
}
|
||||||
|
ui.label(egui::RichText::new(&card.id).small().monospace().weak());
|
||||||
|
if let Some(date) = card.upload_date.as_deref().map(format_upload_date) {
|
||||||
|
if !date.is_empty() {
|
||||||
|
ui.label(egui::RichText::new("·").weak());
|
||||||
|
ui.label(egui::RichText::new(date).small().weak());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(secs) = card.duration_secs {
|
||||||
|
ui.label(egui::RichText::new("·").weak());
|
||||||
|
ui.label(egui::RichText::new(format_duration(secs)).small().weak());
|
||||||
|
}
|
||||||
|
if let Some(bytes) = card.file_size {
|
||||||
|
ui.label(egui::RichText::new("·").weak());
|
||||||
|
ui.label(egui::RichText::new(format_size(bytes)).small().weak());
|
||||||
|
}
|
||||||
|
if card.has_live_chat {
|
||||||
|
ui.label(egui::RichText::new("· 💬").small().weak());
|
||||||
|
}
|
||||||
|
if card.video_path.is_none() {
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new("· no video file")
|
||||||
|
.small()
|
||||||
|
.color(egui::Color32::from_rgb(200, 140, 90)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if self.bulk_mode {
|
||||||
|
let chk_label = if bulk_checked { "☑" } else { "☐" };
|
||||||
|
if ui.small_button(chk_label).clicked() {
|
||||||
|
*clicked_card = true; // handled below
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if card.video_path.is_some() && ui.small_button("▶ Play").clicked() {
|
||||||
|
*play_card = true;
|
||||||
|
}
|
||||||
|
if let Some(pos) = card.resume_pos {
|
||||||
|
if pos > 5.0 && ui.small_button(format!("⏩ {}", format_duration(pos))).clicked() {
|
||||||
|
*play_card = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ui.small_button("Details").clicked() {
|
||||||
|
*clicked_card = true;
|
||||||
|
}
|
||||||
|
let w_label = if card.watched { "✓" } else { "○" };
|
||||||
|
if ui.small_button(w_label).on_hover_text("Toggle watched").clicked() {
|
||||||
|
*toggle_watched_card = true;
|
||||||
|
}
|
||||||
|
let fav_label = if card.favourite { "★" } else { "☆" };
|
||||||
|
if ui.small_button(fav_label).on_hover_text("Toggle favourite").clicked() {
|
||||||
|
*toggle_flag_card = Some("favourite");
|
||||||
|
}
|
||||||
|
let bm_label = if card.bookmark { "🔖" } else { "🔖̲" };
|
||||||
|
if ui.small_button(bm_label).on_hover_text("Toggle bookmark").clicked() {
|
||||||
|
*toggle_flag_card = Some("bookmark");
|
||||||
|
}
|
||||||
|
let wait_label = if card.waiting { "⏳" } else { "⏰" };
|
||||||
|
if ui.small_button(wait_label).on_hover_text("Toggle waiting").clicked() {
|
||||||
|
*toggle_flag_card = Some("waiting");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply the per-card deferred actions (click/play/watch/flag toggles).
|
||||||
|
fn apply_card_actions(
|
||||||
|
&mut self,
|
||||||
|
card: &Card,
|
||||||
|
clicked_card: bool,
|
||||||
|
play_card: bool,
|
||||||
|
toggle_watched_card: bool,
|
||||||
|
toggle_flag_card: Option<&'static str>,
|
||||||
|
) {
|
||||||
|
if self.bulk_mode {
|
||||||
|
if clicked_card {
|
||||||
|
let id = card.id.clone();
|
||||||
|
if self.bulk_selected.contains(&id) {
|
||||||
|
self.bulk_selected.remove(&id);
|
||||||
|
} else {
|
||||||
|
self.bulk_selected.insert(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if play_card {
|
||||||
|
if let Some(p) = card.video_path.clone() {
|
||||||
|
let id = card.id.clone();
|
||||||
|
self.play_with_tracking(&p, id);
|
||||||
|
}
|
||||||
|
self.selected_video = Some(card.id.clone());
|
||||||
|
} else if clicked_card {
|
||||||
|
self.selected_video = Some(card.id.clone());
|
||||||
|
}
|
||||||
|
if toggle_watched_card {
|
||||||
|
let id = card.id.clone();
|
||||||
|
self.toggle_watched(&id);
|
||||||
|
}
|
||||||
|
if let Some(flag) = toggle_flag_card {
|
||||||
|
let id = card.id.clone();
|
||||||
|
self.toggle_video_flag(&id, flag);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl eframe::App for App {
|
impl eframe::App for App {
|
||||||
|
|
|
||||||
|
|
@ -207,9 +207,14 @@ pub struct UiSection {
|
||||||
/// the built-in Ctrl + / Ctrl - / Ctrl 0 keyboard zoom.
|
/// the built-in Ctrl + / Ctrl - / Ctrl 0 keyboard zoom.
|
||||||
#[serde(default = "default_ui_scale")]
|
#[serde(default = "default_ui_scale")]
|
||||||
pub ui_scale: f32,
|
pub ui_scale: f32,
|
||||||
|
/// Default video-list render mode: "list", "card", or "grid".
|
||||||
|
/// Per-view overrides live in App state (not persisted beyond session).
|
||||||
|
#[serde(default = "default_view_mode")]
|
||||||
|
pub default_view_mode: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_ui_scale() -> f32 { 1.0 }
|
fn default_ui_scale() -> f32 { 1.0 }
|
||||||
|
fn default_view_mode() -> String { "list".to_string() }
|
||||||
|
|
||||||
impl Default for UiSection {
|
impl Default for UiSection {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
|
@ -217,6 +222,7 @@ impl Default for UiSection {
|
||||||
theme: default_theme(),
|
theme: default_theme(),
|
||||||
minimize_to_tray: false,
|
minimize_to_tray: false,
|
||||||
ui_scale: default_ui_scale(),
|
ui_scale: default_ui_scale(),
|
||||||
|
default_view_mode: default_view_mode(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -319,3 +325,22 @@ impl Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ui_section_default_view_mode_is_list() {
|
||||||
|
let ui = UiSection::default();
|
||||||
|
assert_eq!(ui.default_view_mode, "list");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ui_section_round_trips_through_toml() {
|
||||||
|
let ui = UiSection { default_view_mode: "grid".into(), ..Default::default() };
|
||||||
|
let s = toml::to_string(&ui).unwrap();
|
||||||
|
let back: UiSection = toml::from_str(&s).unwrap();
|
||||||
|
assert_eq!(back.default_view_mode, "grid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
554
src/theme.rs
554
src/theme.rs
|
|
@ -5,20 +5,89 @@ pub const THEMES: &[(&str, &str)] = &[
|
||||||
("light", "Light"),
|
("light", "Light"),
|
||||||
("dracula", "Dracula"),
|
("dracula", "Dracula"),
|
||||||
("trans", "Trans"),
|
("trans", "Trans"),
|
||||||
|
// Catacomb / goth
|
||||||
("emo-nocturnal", "Emo: Nocturnal"),
|
("emo-nocturnal", "Emo: Nocturnal"),
|
||||||
("emo-coffin", "Emo: Coffin"),
|
("emo-coffin", "Emo: Coffin"),
|
||||||
("emo-scene-queen", "Emo: Scene Queen"),
|
("emo-scene-queen", "Emo: Scene Queen"),
|
||||||
|
("cemetery-moss", "Cemetery Moss"),
|
||||||
|
("vampire", "Vampire"),
|
||||||
|
("witching-hour", "Witching Hour"),
|
||||||
|
// Neon / retro
|
||||||
|
("cyberpunk", "Cyberpunk"),
|
||||||
|
("synthwave", "Synthwave '84"),
|
||||||
|
("vaporwave", "Vaporwave"),
|
||||||
|
// Dev palettes
|
||||||
|
("nord", "Nord"),
|
||||||
|
("gruvbox", "Gruvbox"),
|
||||||
|
("tokyo-night", "Tokyo Night"),
|
||||||
|
// Cozy / light
|
||||||
|
("paper", "Paper"),
|
||||||
|
("honey", "Honey"),
|
||||||
|
("candlelight", "Candlelight"),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Theme-aware semantic accent colors. egui `Visuals` has no slot for these,
|
||||||
|
/// so each theme exposes them here and the paint code in `app.rs` reads from
|
||||||
|
/// the active snapshot instead of hardcoding RGB values.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||||
|
pub struct ThemeAccents {
|
||||||
|
/// Selection / focus ring (replaces the hardcoded 120,170,230 blue).
|
||||||
|
pub accent: Color32,
|
||||||
|
/// Playing / watched indicator (replaces 110,200,110 green).
|
||||||
|
pub success: Color32,
|
||||||
|
/// Bulk-selection highlight (replaces 180,130,240 purple).
|
||||||
|
pub warning: Color32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Look up the semantic accents for a theme name. Falls back to the dark
|
||||||
|
/// theme's accents for unknown names.
|
||||||
|
pub fn accents_for(name: &str) -> ThemeAccents {
|
||||||
|
match name {
|
||||||
|
"dark" => ThemeAccents { accent: hex(0x7aa2f7), success: hex(0x9ece6a), warning: hex(0xbb9af7) },
|
||||||
|
"light" => ThemeAccents { accent: hex(0x2a5db0), success: hex(0x2e7d32), warning: hex(0x8e44ad) },
|
||||||
|
"dracula" => ThemeAccents { accent: hex(0xbd93f9), success: hex(0x50fa7b), warning: hex(0xff79c6) },
|
||||||
|
"trans" => ThemeAccents { accent: hex(0x55cdfc), success: hex(0x2e7d32), warning: hex(0xf7a8b8) },
|
||||||
|
"emo-nocturnal" => ThemeAccents { accent: hex(0xff0090), success: hex(0x39ff14), warning: hex(0x00f5ff) },
|
||||||
|
"emo-coffin" => ThemeAccents { accent: hex(0x8b0000), success: hex(0x39ff14), warning: hex(0xcc2222) },
|
||||||
|
"emo-scene-queen" => ThemeAccents { accent: hex(0x39ff14), success: hex(0xff00ff), warning: hex(0x00f5ff) },
|
||||||
|
// New themes (added in a later task — values match their palettes).
|
||||||
|
"cyberpunk" => ThemeAccents { accent: hex(0x00fff5), success: hex(0x39ff14), warning: hex(0xff003c) },
|
||||||
|
"synthwave" => ThemeAccents { accent: hex(0xff2a6d), success: hex(0x05d9e8), warning: hex(0xd136a6) },
|
||||||
|
"vaporwave" => ThemeAccents { accent: hex(0x01cdfe), success: hex(0x05ffa1), warning: hex(0xff71ce) },
|
||||||
|
"cemetery-moss" => ThemeAccents { accent: hex(0x7a8a6a), success: hex(0x4a5d3a), warning: hex(0x9a9a8a) },
|
||||||
|
"vampire" => ThemeAccents { accent: hex(0xc9a227), success: hex(0x8b1a2b), warning: hex(0x5c0a1e) },
|
||||||
|
"witching-hour" => ThemeAccents { accent: hex(0x6a4a8b), success: hex(0xb0b8d0), warning: hex(0x1a1a4a) },
|
||||||
|
"nord" => ThemeAccents { accent: hex(0x88c0d0), success: hex(0xa3be8c), warning: hex(0xebcb8b) },
|
||||||
|
"gruvbox" => ThemeAccents { accent: hex(0xfe8019), success: hex(0xb8bb26), warning: hex(0xd3869b) },
|
||||||
|
"tokyo-night" => ThemeAccents { accent: hex(0x7aa2f7), success: hex(0x9ece6a), warning: hex(0xbb9af7) },
|
||||||
|
"paper" => ThemeAccents { accent: hex(0x8b6f3a), success: hex(0x4a6b3a), warning: hex(0xc4a86a) },
|
||||||
|
"honey" => ThemeAccents { accent: hex(0xe8a838), success: hex(0xb87420), warning: hex(0xc97b4a) },
|
||||||
|
"candlelight" => ThemeAccents { accent: hex(0xa8703a), success: hex(0x6b3f1e), warning: hex(0xd9b382) },
|
||||||
|
_ => ThemeAccents { accent: hex(0x7aa2f7), success: hex(0x9ece6a), warning: hex(0xbb9af7) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn apply(ctx: &egui::Context, name: &str) {
|
pub fn apply(ctx: &egui::Context, name: &str) {
|
||||||
let visuals = match name {
|
let visuals = match name {
|
||||||
"light" => egui::Visuals::light(),
|
"light" => light(),
|
||||||
"dracula" => dracula(),
|
"dracula" => dracula(),
|
||||||
"trans" => trans(),
|
"trans" => trans(),
|
||||||
"emo-nocturnal" => emo_nocturnal(),
|
"emo-nocturnal" => emo_nocturnal(),
|
||||||
"emo-coffin" => emo_coffin(),
|
"emo-coffin" => emo_coffin(),
|
||||||
"emo-scene-queen" => emo_scene_queen(),
|
"emo-scene-queen" => emo_scene_queen(),
|
||||||
_ => egui::Visuals::dark(),
|
"cemetery-moss" => cemetery_moss(),
|
||||||
|
"vampire" => vampire(),
|
||||||
|
"witching-hour" => witching_hour(),
|
||||||
|
"cyberpunk" => cyberpunk(),
|
||||||
|
"synthwave" => synthwave(),
|
||||||
|
"vaporwave" => vaporwave(),
|
||||||
|
"nord" => nord(),
|
||||||
|
"gruvbox" => gruvbox(),
|
||||||
|
"tokyo-night" => tokyo_night(),
|
||||||
|
"paper" => paper(),
|
||||||
|
"honey" => honey(),
|
||||||
|
"candlelight" => candlelight(),
|
||||||
|
_ => dark(),
|
||||||
};
|
};
|
||||||
ctx.set_visuals(visuals);
|
ctx.set_visuals(visuals);
|
||||||
}
|
}
|
||||||
|
|
@ -27,6 +96,68 @@ fn hex(v: u32) -> Color32 {
|
||||||
Color32::from_rgb(((v >> 16) & 0xff) as u8, ((v >> 8) & 0xff) as u8, (v & 0xff) as u8)
|
Color32::from_rgb(((v >> 16) & 0xff) as u8, ((v >> 8) & 0xff) as u8, (v & 0xff) as u8)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hand-tuned default dark — true near-black panel, cool steel-blue accent.
|
||||||
|
// Replaces the stock egui::Visuals::dark() so the out-of-box experience
|
||||||
|
// matches the care given to the themed variants.
|
||||||
|
fn dark() -> egui::Visuals {
|
||||||
|
let mut v = egui::Visuals::dark();
|
||||||
|
v.panel_fill = hex(0x14141a);
|
||||||
|
v.window_fill = hex(0x1a1a22);
|
||||||
|
v.extreme_bg_color = hex(0x0a0a0e);
|
||||||
|
v.faint_bg_color = hex(0x1f1f29);
|
||||||
|
v.code_bg_color = hex(0x101016);
|
||||||
|
v.selection.bg_fill = hex(0x2a3a5a);
|
||||||
|
v.selection.stroke = Stroke::new(1.0, hex(0x7aa2f7));
|
||||||
|
v.hyperlink_color = hex(0x7aa2f7);
|
||||||
|
v.override_text_color = None;
|
||||||
|
v.widgets.noninteractive.bg_fill = hex(0x1f1f29);
|
||||||
|
v.widgets.noninteractive.weak_bg_fill = hex(0x181820);
|
||||||
|
v.widgets.noninteractive.fg_stroke = Stroke::new(1.0, hex(0xc8c8d8));
|
||||||
|
v.widgets.noninteractive.bg_stroke = Stroke::new(1.0, hex(0x2a2a36));
|
||||||
|
v.widgets.inactive.bg_fill = hex(0x2a2a36);
|
||||||
|
v.widgets.inactive.weak_bg_fill = hex(0x222230);
|
||||||
|
v.widgets.inactive.fg_stroke = Stroke::new(1.0, hex(0xd0d0e0));
|
||||||
|
v.widgets.hovered.bg_fill = hex(0x3a3a4e);
|
||||||
|
v.widgets.hovered.weak_bg_fill = hex(0x303044);
|
||||||
|
v.widgets.hovered.fg_stroke = Stroke::new(1.5, hex(0xe8e8f8));
|
||||||
|
v.widgets.active.bg_fill = hex(0x7aa2f7);
|
||||||
|
v.widgets.active.weak_bg_fill = hex(0x5a82d7);
|
||||||
|
v.widgets.active.fg_stroke = Stroke::new(2.0, hex(0x14141a));
|
||||||
|
v.widgets.open.bg_fill = hex(0x3a3a4e);
|
||||||
|
v.window_stroke = Stroke::new(1.0, hex(0x2a2a36));
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hand-tuned default light — warm off-white, slate accent.
|
||||||
|
fn light() -> egui::Visuals {
|
||||||
|
let mut v = egui::Visuals::light();
|
||||||
|
v.panel_fill = hex(0xf6f4ef);
|
||||||
|
v.window_fill = hex(0xfefcf7);
|
||||||
|
v.extreme_bg_color = hex(0xffffff);
|
||||||
|
v.faint_bg_color = hex(0xefece4);
|
||||||
|
v.code_bg_color = hex(0xeae6dc);
|
||||||
|
v.selection.bg_fill = hex(0xbcd0ee);
|
||||||
|
v.selection.stroke = Stroke::new(1.0, hex(0x2a5db0));
|
||||||
|
v.hyperlink_color = hex(0x2a5db0);
|
||||||
|
v.override_text_color = None;
|
||||||
|
v.widgets.noninteractive.bg_fill = hex(0xeae6dc);
|
||||||
|
v.widgets.noninteractive.weak_bg_fill = hex(0xf0ede5);
|
||||||
|
v.widgets.noninteractive.fg_stroke = Stroke::new(1.0, hex(0x3a3a3a));
|
||||||
|
v.widgets.noninteractive.bg_stroke = Stroke::new(1.0, hex(0xd0ccbf));
|
||||||
|
v.widgets.inactive.bg_fill = hex(0xdfe0e8);
|
||||||
|
v.widgets.inactive.weak_bg_fill = hex(0xe8e9f0);
|
||||||
|
v.widgets.inactive.fg_stroke = Stroke::new(1.0, hex(0x2a2a2a));
|
||||||
|
v.widgets.hovered.bg_fill = hex(0xcdd6e8);
|
||||||
|
v.widgets.hovered.weak_bg_fill = hex(0xd8e0ee);
|
||||||
|
v.widgets.hovered.fg_stroke = Stroke::new(1.5, hex(0x141414));
|
||||||
|
v.widgets.active.bg_fill = hex(0x2a5db0);
|
||||||
|
v.widgets.active.weak_bg_fill = hex(0x4a7dd0);
|
||||||
|
v.widgets.active.fg_stroke = Stroke::new(2.0, hex(0xffffff));
|
||||||
|
v.widgets.open.bg_fill = hex(0xcdd6e8);
|
||||||
|
v.window_stroke = Stroke::new(1.0, hex(0xd0ccbf));
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
fn dracula() -> egui::Visuals {
|
fn dracula() -> egui::Visuals {
|
||||||
let mut v = egui::Visuals::dark();
|
let mut v = egui::Visuals::dark();
|
||||||
v.panel_fill = hex(0x282a36);
|
v.panel_fill = hex(0x282a36);
|
||||||
|
|
@ -192,3 +323,422 @@ fn emo_scene_queen() -> egui::Visuals {
|
||||||
v.error_fg_color = hex(0xff4444);
|
v.error_fg_color = hex(0xff4444);
|
||||||
v
|
v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Neon / retro ===
|
||||||
|
|
||||||
|
// Magenta + electric cyan on black. Hacker HUD.
|
||||||
|
fn cyberpunk() -> egui::Visuals {
|
||||||
|
let mut v = egui::Visuals::dark();
|
||||||
|
v.panel_fill = hex(0x0a0a12);
|
||||||
|
v.window_fill = hex(0x0e0e18);
|
||||||
|
v.extreme_bg_color = hex(0x050508);
|
||||||
|
v.faint_bg_color = hex(0x14141f);
|
||||||
|
v.code_bg_color = hex(0x08080f);
|
||||||
|
v.selection.bg_fill = hex(0xff003c);
|
||||||
|
v.selection.stroke = Stroke::new(1.0, hex(0x00fff5));
|
||||||
|
v.hyperlink_color = hex(0x00fff5);
|
||||||
|
v.override_text_color = None;
|
||||||
|
v.widgets.noninteractive.bg_fill = hex(0x14141f);
|
||||||
|
v.widgets.noninteractive.weak_bg_fill = hex(0x0f0f18);
|
||||||
|
v.widgets.noninteractive.fg_stroke = Stroke::new(1.0, hex(0xc8c8e0));
|
||||||
|
v.widgets.noninteractive.bg_stroke = Stroke::new(1.0, hex(0x2a2a3a));
|
||||||
|
v.widgets.inactive.bg_fill = hex(0x1f0018);
|
||||||
|
v.widgets.inactive.weak_bg_fill = hex(0x180013);
|
||||||
|
v.widgets.inactive.fg_stroke = Stroke::new(1.0, hex(0xd0d0e8));
|
||||||
|
v.widgets.hovered.bg_fill = hex(0x6a0044);
|
||||||
|
v.widgets.hovered.weak_bg_fill = hex(0x500034);
|
||||||
|
v.widgets.hovered.fg_stroke = Stroke::new(1.5, hex(0x00fff5));
|
||||||
|
v.widgets.active.bg_fill = hex(0xff003c);
|
||||||
|
v.widgets.active.weak_bg_fill = hex(0xcc0030);
|
||||||
|
v.widgets.active.fg_stroke = Stroke::new(2.0, hex(0xffffff));
|
||||||
|
v.widgets.open.bg_fill = hex(0x6a0044);
|
||||||
|
v.window_stroke = Stroke::new(1.0, hex(0xff003c));
|
||||||
|
v.warn_fg_color = hex(0xfcee0a);
|
||||||
|
v.error_fg_color = hex(0xff003c);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sunset gradient on deep indigo.
|
||||||
|
fn synthwave() -> egui::Visuals {
|
||||||
|
let mut v = egui::Visuals::dark();
|
||||||
|
v.panel_fill = hex(0x2b0a3d);
|
||||||
|
v.window_fill = hex(0x330a48);
|
||||||
|
v.extreme_bg_color = hex(0x1a0529);
|
||||||
|
v.faint_bg_color = hex(0x3a1055);
|
||||||
|
v.code_bg_color = hex(0x220833);
|
||||||
|
v.selection.bg_fill = hex(0xff2a6d);
|
||||||
|
v.selection.stroke = Stroke::new(1.0, hex(0x05d9e8));
|
||||||
|
v.hyperlink_color = hex(0x05d9e8);
|
||||||
|
v.override_text_color = None;
|
||||||
|
v.widgets.noninteractive.bg_fill = hex(0x3a1055);
|
||||||
|
v.widgets.noninteractive.weak_bg_fill = hex(0x2e0c45);
|
||||||
|
v.widgets.noninteractive.fg_stroke = Stroke::new(1.0, hex(0xe8c0e0));
|
||||||
|
v.widgets.noninteractive.bg_stroke = Stroke::new(1.0, hex(0x5a1a7a));
|
||||||
|
v.widgets.inactive.bg_fill = hex(0x4a1466);
|
||||||
|
v.widgets.inactive.weak_bg_fill = hex(0x3c1055);
|
||||||
|
v.widgets.inactive.fg_stroke = Stroke::new(1.0, hex(0xf0d0e8));
|
||||||
|
v.widgets.hovered.bg_fill = hex(0x7a1a4a);
|
||||||
|
v.widgets.hovered.weak_bg_fill = hex(0x60143a);
|
||||||
|
v.widgets.hovered.fg_stroke = Stroke::new(1.5, hex(0xff9a3c));
|
||||||
|
v.widgets.active.bg_fill = hex(0xff2a6d);
|
||||||
|
v.widgets.active.weak_bg_fill = hex(0xcc2055);
|
||||||
|
v.widgets.active.fg_stroke = Stroke::new(2.0, hex(0xffffff));
|
||||||
|
v.widgets.open.bg_fill = hex(0x7a1a4a);
|
||||||
|
v.window_stroke = Stroke::new(1.0, hex(0xff2a6d));
|
||||||
|
v.warn_fg_color = hex(0xff9a3c);
|
||||||
|
v.error_fg_color = hex(0xff2a6d);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pastel pink + cyan + lavender on deep plum.
|
||||||
|
fn vaporwave() -> egui::Visuals {
|
||||||
|
let mut v = egui::Visuals::dark();
|
||||||
|
v.panel_fill = hex(0x1a0033);
|
||||||
|
v.window_fill = hex(0x200040);
|
||||||
|
v.extreme_bg_color = hex(0x10001f);
|
||||||
|
v.faint_bg_color = hex(0x28004e);
|
||||||
|
v.code_bg_color = hex(0x150028);
|
||||||
|
v.selection.bg_fill = hex(0xb967ff);
|
||||||
|
v.selection.stroke = Stroke::new(1.0, hex(0x01cdfe));
|
||||||
|
v.hyperlink_color = hex(0x01cdfe);
|
||||||
|
v.override_text_color = None;
|
||||||
|
v.widgets.noninteractive.bg_fill = hex(0x28004e);
|
||||||
|
v.widgets.noninteractive.weak_bg_fill = hex(0x1f003c);
|
||||||
|
v.widgets.noninteractive.fg_stroke = Stroke::new(1.0, hex(0xe0c0ff));
|
||||||
|
v.widgets.noninteractive.bg_stroke = Stroke::new(1.0, hex(0x4a0070));
|
||||||
|
v.widgets.inactive.bg_fill = hex(0x350060);
|
||||||
|
v.widgets.inactive.weak_bg_fill = hex(0x2a004f);
|
||||||
|
v.widgets.inactive.fg_stroke = Stroke::new(1.0, hex(0xf0d8ff));
|
||||||
|
v.widgets.hovered.bg_fill = hex(0x55008a);
|
||||||
|
v.widgets.hovered.weak_bg_fill = hex(0x440070);
|
||||||
|
v.widgets.hovered.fg_stroke = Stroke::new(1.5, hex(0x01cdfe));
|
||||||
|
v.widgets.active.bg_fill = hex(0xff71ce);
|
||||||
|
v.widgets.active.weak_bg_fill = hex(0xcc5aa6);
|
||||||
|
v.widgets.active.fg_stroke = Stroke::new(2.0, hex(0x1a0033));
|
||||||
|
v.widgets.open.bg_fill = hex(0x55008a);
|
||||||
|
v.window_stroke = Stroke::new(1.0, hex(0xff71ce));
|
||||||
|
v.warn_fg_color = hex(0x05ffa1);
|
||||||
|
v.error_fg_color = hex(0xff71ce);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Catacomb / goth ===
|
||||||
|
|
||||||
|
// Weathered stone + mossy green + bone.
|
||||||
|
fn cemetery_moss() -> egui::Visuals {
|
||||||
|
let mut v = egui::Visuals::dark();
|
||||||
|
v.panel_fill = hex(0x1a1f1a);
|
||||||
|
v.window_fill = hex(0x1e241e);
|
||||||
|
v.extreme_bg_color = hex(0x0f140f);
|
||||||
|
v.faint_bg_color = hex(0x242a24);
|
||||||
|
v.code_bg_color = hex(0x141814);
|
||||||
|
v.selection.bg_fill = hex(0x4a5d3a);
|
||||||
|
v.selection.stroke = Stroke::new(1.0, hex(0x7a8a6a));
|
||||||
|
v.hyperlink_color = hex(0x9aa86a);
|
||||||
|
v.override_text_color = None;
|
||||||
|
v.widgets.noninteractive.bg_fill = hex(0x242a24);
|
||||||
|
v.widgets.noninteractive.weak_bg_fill = hex(0x1f241f);
|
||||||
|
v.widgets.noninteractive.fg_stroke = Stroke::new(1.0, hex(0xb8b8a8));
|
||||||
|
v.widgets.noninteractive.bg_stroke = Stroke::new(1.0, hex(0x363a30));
|
||||||
|
v.widgets.inactive.bg_fill = hex(0x2a302a);
|
||||||
|
v.widgets.inactive.weak_bg_fill = hex(0x232823);
|
||||||
|
v.widgets.inactive.fg_stroke = Stroke::new(1.0, hex(0xc8c8b8));
|
||||||
|
v.widgets.hovered.bg_fill = hex(0x3a4530);
|
||||||
|
v.widgets.hovered.weak_bg_fill = hex(0x2e3826);
|
||||||
|
v.widgets.hovered.fg_stroke = Stroke::new(1.5, hex(0xaac08a));
|
||||||
|
v.widgets.active.bg_fill = hex(0x4a5d3a);
|
||||||
|
v.widgets.active.weak_bg_fill = hex(0x3a4a2e);
|
||||||
|
v.widgets.active.fg_stroke = Stroke::new(2.0, hex(0xd4d0c0));
|
||||||
|
v.widgets.open.bg_fill = hex(0x3a4530);
|
||||||
|
v.window_stroke = Stroke::new(1.0, hex(0x4a5d3a));
|
||||||
|
v.warn_fg_color = hex(0xc9a227);
|
||||||
|
v.error_fg_color = hex(0x8b1a2b);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deep wine burgundy + antique gold + black.
|
||||||
|
fn vampire() -> egui::Visuals {
|
||||||
|
let mut v = egui::Visuals::dark();
|
||||||
|
v.panel_fill = hex(0x0d0006);
|
||||||
|
v.window_fill = hex(0x120008);
|
||||||
|
v.extreme_bg_color = hex(0x070003);
|
||||||
|
v.faint_bg_color = hex(0x18000c);
|
||||||
|
v.code_bg_color = hex(0x0a0005);
|
||||||
|
v.selection.bg_fill = hex(0x5c0a1e);
|
||||||
|
v.selection.stroke = Stroke::new(1.0, hex(0xc9a227));
|
||||||
|
v.hyperlink_color = hex(0xc9a227);
|
||||||
|
v.override_text_color = None;
|
||||||
|
v.widgets.noninteractive.bg_fill = hex(0x18000c);
|
||||||
|
v.widgets.noninteractive.weak_bg_fill = hex(0x130009);
|
||||||
|
v.widgets.noninteractive.fg_stroke = Stroke::new(1.0, hex(0xd8c8a8));
|
||||||
|
v.widgets.noninteractive.bg_stroke = Stroke::new(1.0, hex(0x3a0014));
|
||||||
|
v.widgets.inactive.bg_fill = hex(0x200010);
|
||||||
|
v.widgets.inactive.weak_bg_fill = hex(0x19000c);
|
||||||
|
v.widgets.inactive.fg_stroke = Stroke::new(1.0, hex(0xe8d8b8));
|
||||||
|
v.widgets.hovered.bg_fill = hex(0x400018);
|
||||||
|
v.widgets.hovered.weak_bg_fill = hex(0x300012);
|
||||||
|
v.widgets.hovered.fg_stroke = Stroke::new(1.5, hex(0xc9a227));
|
||||||
|
v.widgets.active.bg_fill = hex(0x8b1a2b);
|
||||||
|
v.widgets.active.weak_bg_fill = hex(0x700020);
|
||||||
|
v.widgets.active.fg_stroke = Stroke::new(2.0, hex(0xe8d8b8));
|
||||||
|
v.widgets.open.bg_fill = hex(0x400018);
|
||||||
|
v.window_stroke = Stroke::new(1.0, hex(0x8b1a2b));
|
||||||
|
v.warn_fg_color = hex(0xc9a227);
|
||||||
|
v.error_fg_color = hex(0x8b1a2b);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Midnight indigo + moonlight silver + arcane violet.
|
||||||
|
fn witching_hour() -> egui::Visuals {
|
||||||
|
let mut v = egui::Visuals::dark();
|
||||||
|
v.panel_fill = hex(0x0a0a1f);
|
||||||
|
v.window_fill = hex(0x0e0e28);
|
||||||
|
v.extreme_bg_color = hex(0x05050f);
|
||||||
|
v.faint_bg_color = hex(0x14142e);
|
||||||
|
v.code_bg_color = hex(0x08081a);
|
||||||
|
v.selection.bg_fill = hex(0x1a1a4a);
|
||||||
|
v.selection.stroke = Stroke::new(1.0, hex(0xb0b8d0));
|
||||||
|
v.hyperlink_color = hex(0xb0b8d0);
|
||||||
|
v.override_text_color = None;
|
||||||
|
v.widgets.noninteractive.bg_fill = hex(0x14142e);
|
||||||
|
v.widgets.noninteractive.weak_bg_fill = hex(0x101024);
|
||||||
|
v.widgets.noninteractive.fg_stroke = Stroke::new(1.0, hex(0xc0c8e0));
|
||||||
|
v.widgets.noninteractive.bg_stroke = Stroke::new(1.0, hex(0x2a2a55));
|
||||||
|
v.widgets.inactive.bg_fill = hex(0x1c1c3c);
|
||||||
|
v.widgets.inactive.weak_bg_fill = hex(0x161630);
|
||||||
|
v.widgets.inactive.fg_stroke = Stroke::new(1.0, hex(0xd0d8f0));
|
||||||
|
v.widgets.hovered.bg_fill = hex(0x2a2a5a);
|
||||||
|
v.widgets.hovered.weak_bg_fill = hex(0x20204a);
|
||||||
|
v.widgets.hovered.fg_stroke = Stroke::new(1.5, hex(0x6a4a8b));
|
||||||
|
v.widgets.active.bg_fill = hex(0x6a4a8b);
|
||||||
|
v.widgets.active.weak_bg_fill = hex(0x523a72);
|
||||||
|
v.widgets.active.fg_stroke = Stroke::new(2.0, hex(0xe8e8f8));
|
||||||
|
v.widgets.open.bg_fill = hex(0x2a2a5a);
|
||||||
|
v.window_stroke = Stroke::new(1.0, hex(0x6a4a8b));
|
||||||
|
v.warn_fg_color = hex(0xc9a227);
|
||||||
|
v.error_fg_color = hex(0x8b1a2b);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Dev palettes ===
|
||||||
|
|
||||||
|
// Arctic blues & greys.
|
||||||
|
fn nord() -> egui::Visuals {
|
||||||
|
let mut v = egui::Visuals::dark();
|
||||||
|
v.panel_fill = hex(0x2e3440);
|
||||||
|
v.window_fill = hex(0x3b4252);
|
||||||
|
v.extreme_bg_color = hex(0x1e222a);
|
||||||
|
v.faint_bg_color = hex(0x3b4252);
|
||||||
|
v.code_bg_color = hex(0x242933);
|
||||||
|
v.selection.bg_fill = hex(0x434c5e);
|
||||||
|
v.selection.stroke = Stroke::new(1.0, hex(0x88c0d0));
|
||||||
|
v.hyperlink_color = hex(0x88c0d0);
|
||||||
|
v.override_text_color = None;
|
||||||
|
v.widgets.noninteractive.bg_fill = hex(0x3b4252);
|
||||||
|
v.widgets.noninteractive.weak_bg_fill = hex(0x343b48);
|
||||||
|
v.widgets.noninteractive.fg_stroke = Stroke::new(1.0, hex(0xd8dee9));
|
||||||
|
v.widgets.noninteractive.bg_stroke = Stroke::new(1.0, hex(0x434c5e));
|
||||||
|
v.widgets.inactive.bg_fill = hex(0x434c5e);
|
||||||
|
v.widgets.inactive.weak_bg_fill = hex(0x3c4454);
|
||||||
|
v.widgets.inactive.fg_stroke = Stroke::new(1.0, hex(0xe5e9f0));
|
||||||
|
v.widgets.hovered.bg_fill = hex(0x4c566a);
|
||||||
|
v.widgets.hovered.weak_bg_fill = hex(0x424c60);
|
||||||
|
v.widgets.hovered.fg_stroke = Stroke::new(1.5, hex(0x88c0d0));
|
||||||
|
v.widgets.active.bg_fill = hex(0x88c0d0);
|
||||||
|
v.widgets.active.weak_bg_fill = hex(0x6fa6ba);
|
||||||
|
v.widgets.active.fg_stroke = Stroke::new(2.0, hex(0x2e3440));
|
||||||
|
v.widgets.open.bg_fill = hex(0x4c566a);
|
||||||
|
v.window_stroke = Stroke::new(1.0, hex(0x4c566a));
|
||||||
|
v.warn_fg_color = hex(0xebcb8b);
|
||||||
|
v.error_fg_color = hex(0xbf616a);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warm earthy retro groove.
|
||||||
|
fn gruvbox() -> egui::Visuals {
|
||||||
|
let mut v = egui::Visuals::dark();
|
||||||
|
v.panel_fill = hex(0x282828);
|
||||||
|
v.window_fill = hex(0x32302f);
|
||||||
|
v.extreme_bg_color = hex(0x1d2021);
|
||||||
|
v.faint_bg_color = hex(0x32302f);
|
||||||
|
v.code_bg_color = hex(0x1d2021);
|
||||||
|
v.selection.bg_fill = hex(0x504945);
|
||||||
|
v.selection.stroke = Stroke::new(1.0, hex(0xfe8019));
|
||||||
|
v.hyperlink_color = hex(0x83a598);
|
||||||
|
v.override_text_color = None;
|
||||||
|
v.widgets.noninteractive.bg_fill = hex(0x32302f);
|
||||||
|
v.widgets.noninteractive.weak_bg_fill = hex(0x2c2a29);
|
||||||
|
v.widgets.noninteractive.fg_stroke = Stroke::new(1.0, hex(0xd5c4a1));
|
||||||
|
v.widgets.noninteractive.bg_stroke = Stroke::new(1.0, hex(0x504945));
|
||||||
|
v.widgets.inactive.bg_fill = hex(0x504945);
|
||||||
|
v.widgets.inactive.weak_bg_fill = hex(0x44403c);
|
||||||
|
v.widgets.inactive.fg_stroke = Stroke::new(1.0, hex(0xebdbb2));
|
||||||
|
v.widgets.hovered.bg_fill = hex(0x665c54);
|
||||||
|
v.widgets.hovered.weak_bg_fill = hex(0x585048);
|
||||||
|
v.widgets.hovered.fg_stroke = Stroke::new(1.5, hex(0xfe8019));
|
||||||
|
v.widgets.active.bg_fill = hex(0xd65d0e);
|
||||||
|
v.widgets.active.weak_bg_fill = hex(0xaf4f0b);
|
||||||
|
v.widgets.active.fg_stroke = Stroke::new(2.0, hex(0xebdbb2));
|
||||||
|
v.widgets.open.bg_fill = hex(0x665c54);
|
||||||
|
v.window_stroke = Stroke::new(1.0, hex(0x665c54));
|
||||||
|
v.warn_fg_color = hex(0xfe9d44);
|
||||||
|
v.error_fg_color = hex(0xfb4934);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tokyo city lights. Blue/purple, clean.
|
||||||
|
fn tokyo_night() -> egui::Visuals {
|
||||||
|
let mut v = egui::Visuals::dark();
|
||||||
|
v.panel_fill = hex(0x1a1b26);
|
||||||
|
v.window_fill = hex(0x20202f);
|
||||||
|
v.extreme_bg_color = hex(0x12121c);
|
||||||
|
v.faint_bg_color = hex(0x24253a);
|
||||||
|
v.code_bg_color = hex(0x16161e);
|
||||||
|
v.selection.bg_fill = hex(0x33415c);
|
||||||
|
v.selection.stroke = Stroke::new(1.0, hex(0x7aa2f7));
|
||||||
|
v.hyperlink_color = hex(0x7aa2f7);
|
||||||
|
v.override_text_color = None;
|
||||||
|
v.widgets.noninteractive.bg_fill = hex(0x24253a);
|
||||||
|
v.widgets.noninteractive.weak_bg_fill = hex(0x1f2030);
|
||||||
|
v.widgets.noninteractive.fg_stroke = Stroke::new(1.0, hex(0xa9b1d6));
|
||||||
|
v.widgets.noninteractive.bg_stroke = Stroke::new(1.0, hex(0x3b4261));
|
||||||
|
v.widgets.inactive.bg_fill = hex(0x2a2b3e);
|
||||||
|
v.widgets.inactive.weak_bg_fill = hex(0x232436);
|
||||||
|
v.widgets.inactive.fg_stroke = Stroke::new(1.0, hex(0xc0caf5));
|
||||||
|
v.widgets.hovered.bg_fill = hex(0x363b54);
|
||||||
|
v.widgets.hovered.weak_bg_fill = hex(0x2d324a);
|
||||||
|
v.widgets.hovered.fg_stroke = Stroke::new(1.5, hex(0x7aa2f7));
|
||||||
|
v.widgets.active.bg_fill = hex(0x7aa2f7);
|
||||||
|
v.widgets.active.weak_bg_fill = hex(0x5c82d4);
|
||||||
|
v.widgets.active.fg_stroke = Stroke::new(2.0, hex(0x1a1b26));
|
||||||
|
v.widgets.open.bg_fill = hex(0x363b54);
|
||||||
|
v.window_stroke = Stroke::new(1.0, hex(0x3b4261));
|
||||||
|
v.warn_fg_color = hex(0xe0af68);
|
||||||
|
v.error_fg_color = hex(0xf7768e);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Cozy / light ===
|
||||||
|
|
||||||
|
// Aged paper + sepia ink.
|
||||||
|
fn paper() -> egui::Visuals {
|
||||||
|
let mut v = egui::Visuals::light();
|
||||||
|
v.panel_fill = hex(0xf4ecd8);
|
||||||
|
v.window_fill = hex(0xf8f0dc);
|
||||||
|
v.extreme_bg_color = hex(0xfef8e8);
|
||||||
|
v.faint_bg_color = hex(0xefe5cc);
|
||||||
|
v.code_bg_color = hex(0xe8dcc0);
|
||||||
|
v.selection.bg_fill = hex(0xd9c896);
|
||||||
|
v.selection.stroke = Stroke::new(1.0, hex(0x8b6f3a));
|
||||||
|
v.hyperlink_color = hex(0x6b4f2a);
|
||||||
|
v.override_text_color = None;
|
||||||
|
v.widgets.noninteractive.bg_fill = hex(0xefe5cc);
|
||||||
|
v.widgets.noninteractive.weak_bg_fill = hex(0xf2e9d2);
|
||||||
|
v.widgets.noninteractive.fg_stroke = Stroke::new(1.0, hex(0x3d2b1f));
|
||||||
|
v.widgets.noninteractive.bg_stroke = Stroke::new(1.0, hex(0xc4a86a));
|
||||||
|
v.widgets.inactive.bg_fill = hex(0xe2d4b0);
|
||||||
|
v.widgets.inactive.weak_bg_fill = hex(0xeadab4);
|
||||||
|
v.widgets.inactive.fg_stroke = Stroke::new(1.0, hex(0x2d1f15));
|
||||||
|
v.widgets.hovered.bg_fill = hex(0xd6c498);
|
||||||
|
v.widgets.hovered.weak_bg_fill = hex(0xddcaa8);
|
||||||
|
v.widgets.hovered.fg_stroke = Stroke::new(1.5, hex(0x3d2b1f));
|
||||||
|
v.widgets.active.bg_fill = hex(0x8b6f3a);
|
||||||
|
v.widgets.active.weak_bg_fill = hex(0xa18450);
|
||||||
|
v.widgets.active.fg_stroke = Stroke::new(2.0, hex(0xf4ecd8));
|
||||||
|
v.widgets.open.bg_fill = hex(0xd6c498);
|
||||||
|
v.window_stroke = Stroke::new(1.0, hex(0xc4a86a));
|
||||||
|
v.warn_fg_color = hex(0xb87420);
|
||||||
|
v.error_fg_color = hex(0x9a3a1a);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warm amber + gold + cream.
|
||||||
|
fn honey() -> egui::Visuals {
|
||||||
|
let mut v = egui::Visuals::light();
|
||||||
|
v.panel_fill = hex(0xfff4e0);
|
||||||
|
v.window_fill = hex(0xfff9ec);
|
||||||
|
v.extreme_bg_color = hex(0xffffff);
|
||||||
|
v.faint_bg_color = hex(0xffecc8);
|
||||||
|
v.code_bg_color = hex(0xffe6b8);
|
||||||
|
v.selection.bg_fill = hex(0xffd97a);
|
||||||
|
v.selection.stroke = Stroke::new(1.0, hex(0xb87420));
|
||||||
|
v.hyperlink_color = hex(0x9a5a10);
|
||||||
|
v.override_text_color = None;
|
||||||
|
v.widgets.noninteractive.bg_fill = hex(0xffecc8);
|
||||||
|
v.widgets.noninteractive.weak_bg_fill = hex(0xfff2d4);
|
||||||
|
v.widgets.noninteractive.fg_stroke = Stroke::new(1.0, hex(0x5c3818));
|
||||||
|
v.widgets.noninteractive.bg_stroke = Stroke::new(1.0, hex(0xe8a838));
|
||||||
|
v.widgets.inactive.bg_fill = hex(0xffdf9c);
|
||||||
|
v.widgets.inactive.weak_bg_fill = hex(0xffe6b0);
|
||||||
|
v.widgets.inactive.fg_stroke = Stroke::new(1.0, hex(0x4a2c10));
|
||||||
|
v.widgets.hovered.bg_fill = hex(0xffd166);
|
||||||
|
v.widgets.hovered.weak_bg_fill = hex(0xffdb84);
|
||||||
|
v.widgets.hovered.fg_stroke = Stroke::new(1.5, hex(0x4a2c10));
|
||||||
|
v.widgets.active.bg_fill = hex(0xe8a838);
|
||||||
|
v.widgets.active.weak_bg_fill = hex(0xc8902a);
|
||||||
|
v.widgets.active.fg_stroke = Stroke::new(2.0, hex(0xfff4e0));
|
||||||
|
v.widgets.open.bg_fill = hex(0xffd166);
|
||||||
|
v.window_stroke = Stroke::new(1.0, hex(0xe8a838));
|
||||||
|
v.warn_fg_color = hex(0xb87420);
|
||||||
|
v.error_fg_color = hex(0xa83a1a);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dim warm glow + toasted brown.
|
||||||
|
fn candlelight() -> egui::Visuals {
|
||||||
|
let mut v = egui::Visuals::light();
|
||||||
|
v.panel_fill = hex(0xf2e6d0);
|
||||||
|
v.window_fill = hex(0xf6ecd9);
|
||||||
|
v.extreme_bg_color = hex(0xfbf3e2);
|
||||||
|
v.faint_bg_color = hex(0xead8b8);
|
||||||
|
v.code_bg_color = hex(0xe2cea0);
|
||||||
|
v.selection.bg_fill = hex(0xd9b382);
|
||||||
|
v.selection.stroke = Stroke::new(1.0, hex(0x6b3f1e));
|
||||||
|
v.hyperlink_color = hex(0x5a3418);
|
||||||
|
v.override_text_color = None;
|
||||||
|
v.widgets.noninteractive.bg_fill = hex(0xead8b8);
|
||||||
|
v.widgets.noninteractive.weak_bg_fill = hex(0xeedec2);
|
||||||
|
v.widgets.noninteractive.fg_stroke = Stroke::new(1.0, hex(0x3a2818));
|
||||||
|
v.widgets.noninteractive.bg_stroke = Stroke::new(1.0, hex(0xa8703a));
|
||||||
|
v.widgets.inactive.bg_fill = hex(0xddc498);
|
||||||
|
v.widgets.inactive.weak_bg_fill = hex(0xe4ceaa);
|
||||||
|
v.widgets.inactive.fg_stroke = Stroke::new(1.0, hex(0x2d1a0c));
|
||||||
|
v.widgets.hovered.bg_fill = hex(0xd2ac6e);
|
||||||
|
v.widgets.hovered.weak_bg_fill = hex(0xdab880);
|
||||||
|
v.widgets.hovered.fg_stroke = Stroke::new(1.5, hex(0x2d1a0c));
|
||||||
|
v.widgets.active.bg_fill = hex(0xa8703a);
|
||||||
|
v.widgets.active.weak_bg_fill = hex(0x8c5a2c);
|
||||||
|
v.widgets.active.fg_stroke = Stroke::new(2.0, hex(0xf2e6d0));
|
||||||
|
v.widgets.open.bg_fill = hex(0xd2ac6e);
|
||||||
|
v.window_stroke = Stroke::new(1.0, hex(0xa8703a));
|
||||||
|
v.warn_fg_color = hex(0xb87420);
|
||||||
|
v.error_fg_color = hex(0x9a3a1a);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accents_for_known_theme_returns_nonzero() {
|
||||||
|
let a = accents_for("dark");
|
||||||
|
assert_ne!(a.accent, Color32::TRANSPARENT);
|
||||||
|
assert_ne!(a.success, Color32::TRANSPARENT);
|
||||||
|
assert_ne!(a.warning, Color32::TRANSPARENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accents_for_unknown_theme_falls_back() {
|
||||||
|
let a = accents_for("this-theme-does-not-exist");
|
||||||
|
let dark = accents_for("dark");
|
||||||
|
assert_eq!(a.accent, dark.accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn accents_differ_across_themes() {
|
||||||
|
let dark = accents_for("dark");
|
||||||
|
let light = accents_for("light");
|
||||||
|
assert_ne!(dark.accent, light.accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue