7.4 主题与皮肤

> **本节目标**:掌握 RML 的主题系统——CSS 变量、主题切换、暗色模式、自定义主题。

7.4.1 主题系统的设计

RML 通过 CSS 变量实现主题系统:

┌─────────────────────────────────────────┐
│              主题系统                     │
│                                         │
│  ┌─────────────┐  ┌─────────────┐       │
│  │  亮色主题    │  │  暗色主题    │       │
│  │  light.css  │  │  dark.css   │       │
│  └─────────────┘  └─────────────┘       │
│         │                 │             │
│         └────────┬────────┘             │
│                  ▼                      │
│         ┌────────────────┐              │
│         │  CSS 变量       │              │
│         │  var(--color)  │              │
│         └────────────────┘              │
│                  │                      │
│                  ▼                      │
│         ┌────────────────┐              │
│         │  组件样式       │              │
│         │  使用变量       │              │
│         └────────────────┘              │
└─────────────────────────────────────────┘

7.4.2 定义主题

主题变量

/* src/styles/themes/light.css */
:root {
    /* 颜色 */
    --color-primary: #007bff;
    --color-primary-hover: #0056b3;
    --color-secondary: #6c757d;
    --color-success: #28a745;
    --color-danger: #dc3545;
    --color-warning: #ffc107;
    --color-info: #17a2b8;

    /* 文本颜色 */
    --color-text: #212529;
    --color-text-muted: #6c757d;
    --color-text-inverse: #ffffff;

    /* 背景颜色 */
    --color-bg: #ffffff;
    --color-bg-alt: #f8f9fa;
    --color-bg-inverse: #343a40;

    /* 边框颜色 */
    --color-border: #dee2e6;
    --color-border-light: #e9ecef;

    /* 字体 */
    --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    --font-size-base: 14px;
    --font-size-sm: 12px;
    --font-size-lg: 18px;
    --font-size-xl: 24px;

    /* 间距 */
    --spacing-unit: 8px;

    /* 圆角 */
    --border-radius: 4px;
    --border-radius-lg: 8px;

    /* 阴影 */
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
    --shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    --shadow-lg: 0 4px 8px rgba(0, 0, 0, 0.15);
}
/* src/styles/themes/dark.css */
:root {
    /* 颜色 */
    --color-primary: #3d8bfd;
    --color-primary-hover: #5a9dfd;
    --color-secondary: #6c757d;
    --color-success: #2dd36f;
    --color-danger: #eb445a;
    --color-warning: #ffc409;
    --color-info: #00d4ff;

    /* 文本颜色 */
    --color-text: #f4f5f8;
    --color-text-muted: #989aa2;
    --color-text-inverse: #000000;

    /* 背景颜色 */
    --color-bg: #1a1b1e;
    --color-bg-alt: #25262b;
    --color-bg-inverse: #f4f5f8;

    /* 边框颜色 */
    --color-border: #343a40;
    --color-border-light: #2c2e33;

    /* 阴影 */
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
    --shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
    --shadow-lg: 0 4px 8px rgba(0, 0, 0, 0.5);
}

在 build.rs 中加载主题

// build.rs
fn main() {
    rml::compile_rml()
        .with_style("styles/themes/light.css")  // 默认主题
        .with_style("styles/themes/dark.css")   // 暗色主题
        .with_style("styles/main.css")          // 主样式
        .compile();
}

7.4.3 使用主题变量

在样式中使用

/* src/styles/main.css */
.button {
    background: var(--color-primary);
    color: var(--color-text-inverse);
    padding: var(--spacing-unit) calc(var(--spacing-unit) * 2);
    border: 1px solid transparent;
    border-radius: var(--border-radius);
    font-size: var(--font-size-base);
    font-family: var(--font-family);
    cursor: pointer;
    box-shadow: var(--shadow-sm);
}

.button:hover {
    background: var(--color-primary-hover);
}

.card {
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--border-radius-lg);
    padding: calc(var(--spacing-unit) * 2);
    box-shadow: var(--shadow);
}

.text-muted {
    color: var(--color-text-muted);
}

在内联样式中使用

<div style="background: var(--color-bg); color: var(--color-text);">
    内容
</div>

7.4.4 主题切换

主题管理器

// src/theme.rs
use rml::prelude::*;
use std::sync::Arc;

#[derive(Clone, PartialEq)]
pub enum Theme {
    Light,
    Dark,
}

impl Theme {
    pub fn name(&self) -> &'static str {
        match self {
            Theme::Light => "light",
            Theme::Dark => "dark",
        }
    }

    pub fn toggle(&self) -> Self {
        match self {
            Theme::Light => Theme::Dark,
            Theme::Dark => Theme::Light,
        }
    }
}

#[derive(IModel)]
pub struct ThemeManager {
    pub current_theme: Theme,
}

impl ThemeManager {
    pub fn new() -> Self {
        Self {
            current_theme: Theme::Light,
        }
    }

    pub fn set_theme(&mut self, theme: Theme, cx: &mut ViewContext<Self>) {
        if self.current_theme != theme {
            self.current_theme = theme.clone();
            cx.set_theme(theme.name());  // 切换主题
            cx.notify();
        }
    }

    pub fn toggle_theme(&mut self, cx: &mut ViewContext<Self>) {
        let new_theme = self.current_theme.toggle();
        self.set_theme(new_theme, cx);
    }
}

在应用中使用

// src/app.rml.rs
use rml::prelude::*;
use crate::theme::{ThemeManager, Theme};

#[derive(IModel)]
#[component]
pub struct App {
    pub theme_manager: Entity<ThemeManager>,
}

impl App {
    pub fn new(cx: &mut AppContext) -> Self {
        Self {
            theme_manager: cx.new_model(|_| ThemeManager::new()),
        }
    }

    #[command]
    pub fn toggle_theme(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
        self.theme_manager.update(cx, |manager, cx| {
            manager.toggle_theme(cx);
        });
    }
}
<!-- src/app.rml -->
<div class="app">
    <header class="app-header">
        <h1>我的应用</h1>
        <button on-click={toggle_theme}>
            {theme_manager.current_theme == Theme::Light ? "🌙" : "☀️"}
        </button>
    </header>

    <main class="app-content">
        <!-- 内容 -->
    </main>
</div>

7.4.5 暗色模式

自动跟随系统

use rml::prelude::*;

impl ThemeManager {
    pub fn new(cx: &mut AppContext) -> Self {
        let system_theme = cx.system_theme();  // 获取系统主题
        Self {
            current_theme: system_theme,
        }
    }

    pub fn follow_system(&mut self, cx: &mut ViewContext<Self>) {
        cx.on_system_theme_change(|theme, cx| {
            cx.set_theme(theme.name());
            cx.notify();
        });
    }
}

手动切换

#[command]
pub fn toggle_theme(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
    let new_theme = match self.current_theme {
        Theme::Light => Theme::Dark,
        Theme::Dark => Theme::Light,
    };
    self.set_theme(new_theme, cx);
}

主题持久化

impl ThemeManager {
    pub fn load_saved_theme(&mut self, cx: &mut ViewContext<Self>) {
        if let Some(saved) = cx.local_storage().get("theme") {
            let theme = match saved.as_str() {
                "dark" => Theme::Dark,
                _ => Theme::Light,
            };
            self.set_theme(theme, cx);
        }
    }

    pub fn set_theme(&mut self, theme: Theme, cx: &mut ViewContext<Self>) {
        if self.current_theme != theme {
            self.current_theme = theme.clone();
            cx.set_theme(theme.name());
            cx.local_storage().set("theme", theme.name());  // 持久化
            cx.notify();
        }
    }
}

7.4.6 自定义主题

用户自定义颜色

#[derive(IModel)]
pub struct ThemeManager {
    pub current_theme: Theme,
    pub custom_primary_color: Option<SharedString>,
}

impl ThemeManager {
    pub fn set_custom_primary_color(&mut self, color: SharedString, cx: &mut ViewContext<Self>) {
        self.custom_primary_color = Some(color.clone());
        cx.set_css_var("--color-primary", &color);
        cx.notify();
    }

    pub fn reset_primary_color(&mut self, cx: &mut ViewContext<Self>) {
        self.custom_primary_color = None;
        cx.remove_css_var("--color-primary");
        cx.notify();
    }
}
<div class="theme-settings">
    <h2>主题设置</h2>

    <div class="setting-group">
        <label>主色调</label>
        <div class="color-options">
            <button
                class="color-option"
                style="background: #007bff;"
                on-click={set_primary_color, '#007bff'}
            ></button>
            <button
                class="color-option"
                style="background: #28a745;"
                on-click={set_primary_color, '#28a745'}
            ></button>
            <button
                class="color-option"
                style="background: #dc3545;"
                on-click={set_primary_color, '#dc3545'}
            ></button>
            <button
                class="color-option"
                style="background: #ffc107;"
                on-click={set_primary_color, '#ffc107'}
            ></button>
        </div>
    </div>

    <button on-click={reset_primary_color}>重置</button>
</div>

7.4.7 主题的作用域

全局主题

// 应用全局主题
cx.set_theme("dark");

局部主题

/* 局部主题 */
.dark-section {
    --color-bg: #1a1b1e;
    --color-text: #f4f5f8;
    --color-border: #343a40;
}

.light-section {
    --color-bg: #ffffff;
    --color-text: #212529;
    --color-border: #dee2e6;
}
<div class="container">
    <div class="dark-section">
        <!-- 这部分使用暗色主题 -->
        <p>暗色区域</p>
    </div>

    <div class="light-section">
        <!-- 这部分使用亮色主题 -->
        <p>亮色区域</p>
    </div>
</div>

7.4.8 主题的设计原则

1. 语义化命名

/* ✅ 语义化命名 */
:root {
    --color-primary: #007bff;
    --color-text: #333;
    --color-bg: #fff;
}

/* ❌ 颜色值命名 */
:root {
    --blue: #007bff;
    --dark-gray: #333;
    --white: #fff;
}

2. 层次化设计

:root {
    /* 基础颜色 */
    --color-primary: #007bff;
    --color-primary-hover: #0056b3;
    --color-primary-active: #004085;
    --color-primary-light: #e7f1ff;

    /* 文本颜色 */
    --color-text: #212529;
    --color-text-secondary: #6c757d;
    --color-text-disabled: #adb5bd;

    /* 背景颜色 */
    --color-bg: #ffffff;
    --color-bg-alt: #f8f9fa;
    --color-bg-disabled: #e9ecef;
}

3. 一致性

:root {
    /* 间距统一使用 8px 倍数 */
    --spacing-xs: 4px;
    --spacing-sm: 8px;
    --spacing-md: 16px;
    --spacing-lg: 24px;
    --spacing-xl: 32px;

    /* 圆角统一 */
    --radius-sm: 2px;
    --radius-md: 4px;
    --radius-lg: 8px;
    --radius-full: 9999px;

    /* 字体大小统一 */
    --text-xs: 12px;
    --text-sm: 14px;
    --text-base: 16px;
    --text-lg: 18px;
    --text-xl: 20px;
    --text-2xl: 24px;
}

7.4.9 完整示例:主题系统

// src/theme.rs
use rml::prelude::*;

#[derive(Clone, PartialEq)]
pub enum Theme {
    Light,
    Dark,
    Auto,
}

impl Theme {
    pub fn name(&self) -> &'static str {
        match self {
            Theme::Light => "light",
            Theme::Dark => "dark",
            Theme::Auto => "auto",
        }
    }

    pub fn from_name(name: &str) -> Self {
        match name {
            "dark" => Theme::Dark,
            "auto" => Theme::Auto,
            _ => Theme::Light,
        }
    }
}

#[derive(IModel)]
pub struct ThemeManager {
    pub current_theme: Theme,
    pub is_following_system: bool,
}

impl ThemeManager {
    pub fn new(cx: &mut AppContext) -> Self {
        let saved_theme = cx
            .local_storage()
            .get("theme")
            .map(|s| Theme::from_name(&s))
            .unwrap_or(Theme::Light);

        Self {
            current_theme: saved_theme.clone(),
            is_following_system: saved_theme == Theme::Auto,
        }
    }

    pub fn set_theme(&mut self, theme: Theme, cx: &mut ViewContext<Self>) {
        self.current_theme = theme.clone();
        self.is_following_system = theme == Theme::Auto;

        let actual_theme = if theme == Theme::Auto {
            Theme::from_name(cx.system_theme().name())
        } else {
            theme.clone()
        };

        cx.set_theme(actual_theme.name());
        cx.local_storage().set("theme", theme.name());
        cx.notify();
    }

    #[command]
    pub fn toggle_theme(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
        let new_theme = match self.current_theme {
            Theme::Light => Theme::Dark,
            Theme::Dark | Theme::Auto => Theme::Light,
        };
        self.set_theme(new_theme, cx);
    }

    pub fn theme_icon(&self) -> &'static str {
        match self.current_theme {
            Theme::Light => "☀️",
            Theme::Dark => "🌙",
            Theme::Auto => "🖥️",
        }
    }

    pub fn theme_label(&self) -> &'static str {
        match self.current_theme {
            Theme::Light => "亮色",
            Theme::Dark => "暗色",
            Theme::Auto => "跟随系统",
        }
    }
}
<!-- src/views/settings.rml -->
<div class="settings-page">
    <h1>设置</h1>

    <div class="setting-section">
        <h2>外观</h2>

        <div class="setting-item">
            <label>主题</label>
            <div class="theme-options">
                <button
                    class="theme-option {current_theme == Theme::Light ? 'active' : ''}"
                    on-click={set_theme, 'light'}
                >
                    ☀️ 亮色
                </button>
                <button
                    class="theme-option {current_theme == Theme::Dark ? 'active' : ''}"
                    on-click={set_theme, 'dark'}
                >
                    🌙 暗色
                </button>
                <button
                    class="theme-option {current_theme == Theme::Auto ? 'active' : ''}"
                    on-click={set_theme, 'auto'}
                >
                    🖥️ 跟随系统
                </button>
            </div>
        </div>
    </div>
</div>
#[command]
pub fn set_theme(&mut self, theme_name: SharedString, _: &ClickEvent, cx: &mut ViewContext<Self>) {
    let theme = Theme::from_name(&theme_name);
    self.theme_manager.update(cx, |manager, cx| {
        manager.set_theme(theme, cx);
    });
}

7.4.10 小结

RML 的主题系统:

- **CSS 变量**:通过 `var(--name)` 实现主题 - **主题切换**:`cx.set_theme(name)` 切换主题 - **暗色模式**:支持手动切换和跟随系统 - **持久化**:通过 `local_storage` 保存主题偏好 - **自定义**:运行时修改 CSS 变量 - **作用域**:全局主题和局部主题

下一节 → [7.5 样式复用](./style-reuse.md)