Skip to main content

icon_theme/
icon_theme.rs

1//! Custom icon-theme demo โ€” a three-way theme switcher.
2//!
3//! Run with:
4//!
5//! ```sh
6//! cargo run --example icon_theme
7//! ```
8//!
9//! A pick-list at the top cycles through three themes:
10//!
11//! * **Unicode** โ€” the crate's default when the `icons` feature
12//!   is off; renders emoji-like Unicode symbols (๐Ÿ“ ๐Ÿ“‚ ๐Ÿ“„ โš  โ–ธ โ–พ).
13//! * **Label** โ€” a custom theme defined below that renders short
14//!   text labels (`[DIR]`, `[FILE]`, โ€ฆ) in the default font.
15//! * **Ascii** โ€” another custom theme using single-character
16//!   ASCII symbols so the row height stays tight.
17//!
18//! This demonstrates the `IconTheme` trait surface end-to-end:
19//! how to implement a theme, how to swap between themes at
20//! runtime (just rebuild the tree with a new
21//! `Arc<dyn IconTheme>`), and how the crate's stock themes fit
22//! into the same trait as your own.
23
24use std::path::PathBuf;
25use std::sync::Arc;
26
27use iced::widget::{column, container, pick_list, row, text};
28use iced::{Element, Length, Task};
29use iced_swdir_tree::{
30    DirectoryFilter, DirectoryTree, DirectoryTreeEvent, IconRole, IconSpec, IconTheme, UnicodeTheme,
31};
32
33/// Custom theme: verbose text labels. Demonstrates returning
34/// owned strings (not just `&'static str`) through `Cow`.
35#[derive(Debug)]
36struct LabelTheme;
37
38impl IconTheme for LabelTheme {
39    fn glyph(&self, role: IconRole) -> IconSpec {
40        // External theme โ†’ include a `_ =>` fallback for future
41        // variants added in a minor release.
42        let s: &'static str = match role {
43            IconRole::FolderClosed => "[D]",
44            IconRole::FolderOpen => "[O]",
45            IconRole::File => "[F]",
46            IconRole::Error => "[!]",
47            IconRole::CaretRight => ">",
48            IconRole::CaretDown => "v",
49            _ => "?",
50        };
51        IconSpec::new(s)
52    }
53}
54
55/// Custom theme: ultra-compact ASCII. Shows that a theme doesn't
56/// need to pull in an icon font โ€” plain text works fine if the
57/// glyphs exist in the default system font.
58#[derive(Debug)]
59struct AsciiTheme;
60
61impl IconTheme for AsciiTheme {
62    fn glyph(&self, role: IconRole) -> IconSpec {
63        let s: &'static str = match role {
64            IconRole::FolderClosed => "+",
65            IconRole::FolderOpen => "-",
66            IconRole::File => ".",
67            IconRole::Error => "!",
68            IconRole::CaretRight => ">",
69            IconRole::CaretDown => "v",
70            _ => "?",
71        };
72        IconSpec::new(s)
73    }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77enum ThemeChoice {
78    Unicode,
79    Label,
80    Ascii,
81}
82
83impl ThemeChoice {
84    const ALL: [ThemeChoice; 3] = [ThemeChoice::Unicode, ThemeChoice::Label, ThemeChoice::Ascii];
85
86    fn to_theme(self) -> Arc<dyn IconTheme> {
87        match self {
88            ThemeChoice::Unicode => Arc::new(UnicodeTheme),
89            ThemeChoice::Label => Arc::new(LabelTheme),
90            ThemeChoice::Ascii => Arc::new(AsciiTheme),
91        }
92    }
93}
94
95impl std::fmt::Display for ThemeChoice {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        f.write_str(match self {
98            ThemeChoice::Unicode => "Unicode",
99            ThemeChoice::Label => "Label",
100            ThemeChoice::Ascii => "Ascii",
101        })
102    }
103}
104
105#[derive(Debug, Clone)]
106enum Message {
107    Tree(DirectoryTreeEvent),
108    ThemePicked(ThemeChoice),
109}
110
111struct App {
112    tree: DirectoryTree,
113    choice: ThemeChoice,
114    root: PathBuf,
115}
116
117impl App {
118    fn new() -> (Self, Task<Message>) {
119        let root = resolve_root();
120        let tree = DirectoryTree::new(root.clone())
121            .with_filter(DirectoryFilter::FilesAndFolders)
122            .with_icon_theme(ThemeChoice::Unicode.to_theme());
123        let mut app = App {
124            tree,
125            choice: ThemeChoice::Unicode,
126            root: root.clone(),
127        };
128        let task = app
129            .tree
130            .update(DirectoryTreeEvent::Toggled(root))
131            .map(Message::Tree);
132        (app, task)
133    }
134
135    fn update(&mut self, msg: Message) -> Task<Message> {
136        match msg {
137            Message::Tree(ev) => self.tree.update(ev).map(Message::Tree),
138            Message::ThemePicked(choice) => {
139                // Swapping a theme today requires rebuilding the
140                // tree (it's set at construction via the builder).
141                // We preserve nothing across the swap โ€” a real app
142                // would carry selection/expansion forward, but
143                // this is a demo so a clean rebuild keeps it
144                // short.
145                self.choice = choice;
146                self.tree = DirectoryTree::new(self.root.clone())
147                    .with_filter(DirectoryFilter::FilesAndFolders)
148                    .with_icon_theme(choice.to_theme());
149                self.tree
150                    .update(DirectoryTreeEvent::Toggled(self.root.clone()))
151                    .map(Message::Tree)
152            }
153        }
154    }
155
156    fn view(&self) -> Element<'_, Message> {
157        let picker = pick_list(
158            &ThemeChoice::ALL[..],
159            Some(self.choice),
160            Message::ThemePicked,
161        );
162        let header = row![text("Theme:").size(13), picker].spacing(8);
163
164        column![
165            header,
166            container(self.tree.view(Message::Tree))
167                .width(Length::Fill)
168                .height(Length::Fill),
169            text(
170                "Switch themes to see the icon trait in action. \
171                 The 'Label' and 'Ascii' themes are defined in \
172                 this example file; 'Unicode' is shipped with \
173                 the crate."
174            )
175            .size(12),
176        ]
177        .spacing(8)
178        .padding(10)
179        .into()
180    }
181}
182
183fn resolve_root() -> PathBuf {
184    if let Some(arg) = std::env::args().nth(1) {
185        return PathBuf::from(arg);
186    }
187    let scratch = std::env::temp_dir().join("iced-swdir-tree-icon-theme-demo");
188    let _ = std::fs::create_dir_all(&scratch);
189    for dir in &["src", "src/widgets", "tests", "docs"] {
190        let _ = std::fs::create_dir_all(scratch.join(dir));
191    }
192    for (path, body) in &[
193        ("Cargo.toml", "[package]\n"),
194        ("README.md", "demo\n"),
195        ("src/main.rs", "fn main() {}\n"),
196        ("src/widgets/button.rs", ""),
197        ("tests/basic.rs", ""),
198        ("docs/ROADMAP.md", ""),
199    ] {
200        let p = scratch.join(path);
201        if !p.exists() {
202            let _ = std::fs::write(p, body);
203        }
204    }
205    scratch
206}
207
208fn main() -> iced::Result {
209    iced::application(App::new, App::update, App::view)
210        .title("iced-swdir-tree ยท icon-theme example")
211        .run()
212}