Skip to main content

iced_swdir_tree/directory_tree/
icon.rs

1//! Icon abstraction: [`IconRole`], [`IconSpec`], [`IconTheme`], and
2//! the two stock themes the crate ships.
3//!
4//! The widget renders icons through an [`IconTheme`] โ€” a trait that
5//! returns an [`IconSpec`] (glyph + optional font + optional size)
6//! for each logical [`IconRole`]. Two stock themes are provided:
7//!
8//! * [`UnicodeTheme`] โ€” always available. Renders short Unicode
9//!   symbols (๐Ÿ“ ๐Ÿ“‚ ๐Ÿ“„ โš  โ–ธ โ–พ) that work in any system font.
10//! * [`LucideTheme`] โ€” available with the `icons` feature flag.
11//!   Renders real lucide vector glyphs via the bundled
12//!   [`crate::LUCIDE_FONT_BYTES`] font.
13//!
14//! Which theme is the default depends on the feature flag: with
15//! `icons` on, it's [`LucideTheme`]; with `icons` off, it's
16//! [`UnicodeTheme`]. Applications can plug in their own theme via
17//! [`DirectoryTree::with_icon_theme`]:
18//!
19//! ```ignore
20//! use std::sync::Arc;
21//! use iced_swdir_tree::{DirectoryTree, IconRole, IconSpec, IconTheme};
22//!
23//! #[derive(Debug)]
24//! struct MyTheme;
25//!
26//! impl IconTheme for MyTheme {
27//!     fn glyph(&self, role: IconRole) -> IconSpec {
28//!         match role {
29//!             IconRole::FolderClosed => IconSpec::new("๐Ÿ“‚"),
30//!             IconRole::FolderOpen => IconSpec::new("๐Ÿ“"),
31//!             IconRole::File => IconSpec::new("ยท"),
32//!             _ => IconSpec::new("?"),
33//!         }
34//!     }
35//! }
36//!
37//! let tree = DirectoryTree::new(".".into())
38//!     .with_icon_theme(Arc::new(MyTheme));
39//! ```
40//!
41//! [`DirectoryTree::with_icon_theme`]: crate::DirectoryTree::with_icon_theme
42
43use std::borrow::Cow;
44
45use iced::Element;
46
47/// Semantic icon identifiers the widget renders.
48///
49/// The widget asks the configured [`IconTheme`] for an [`IconSpec`]
50/// per-role whenever it needs to render a row. Themes are
51/// responsible for producing a reasonable visual for every role.
52///
53/// **This enum is `#[non_exhaustive]`** so future versions can add
54/// roles (`Symlink`, `Hidden`, `Loading`, โ€ฆ) without breaking
55/// external themes' `match` exhaustiveness. External themes should
56/// provide a `_ =>` fallback arm when matching.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
58#[non_exhaustive]
59pub enum IconRole {
60    /// A directory that is currently collapsed.
61    FolderClosed,
62    /// A directory that is currently expanded.
63    FolderOpen,
64    /// A regular file.
65    File,
66    /// A directory we could not list (permission denied, etc.).
67    Error,
68    /// The caret pointing right (collapsed indicator for folders).
69    CaretRight,
70    /// The caret pointing down (expanded indicator for folders).
71    CaretDown,
72}
73
74/// Description of how to render an icon for a particular
75/// [`IconRole`].
76///
77/// Constructed by an [`IconTheme`]. The widget takes the spec and
78/// emits an `iced` text element using `glyph`, optionally setting
79/// the specified `font` and `size`. When `font` is `None`, the
80/// glyph renders in the default iced font. When `size` is `None`,
81/// the widget picks its own default (currently 14).
82///
83/// Use [`IconSpec::new`] + [`IconSpec::with_font`] / [`IconSpec::with_size`]
84/// for ergonomic construction:
85///
86/// ```
87/// # use iced_swdir_tree::IconSpec;
88/// let spec = IconSpec::new("\u{E89C}").with_size(16.0);
89/// assert_eq!(spec.glyph.as_ref(), "\u{E89C}");
90/// ```
91///
92/// Fields are `pub` so `const` themes can construct specs
93/// literally โ€” but note that adding fields here is a breaking
94/// change, so we don't expect to add new fields before a
95/// hypothetical 2.0.
96#[derive(Debug, Clone, PartialEq)]
97pub struct IconSpec {
98    /// The text to render. A single `char` for typical icons;
99    /// longer strings are supported (e.g. ligatures, emoji
100    /// sequences, labels like `"DIR"`).
101    ///
102    /// `Cow<'static, str>` so themes can use `&'static str`
103    /// literals at no allocation cost, while still accepting
104    /// owned `String`s for dynamically constructed glyphs.
105    pub glyph: Cow<'static, str>,
106    /// The font to render `glyph` in, or `None` for the iced
107    /// default. Themes using icon-font packs
108    /// ([lucide-icons](https://lucide.dev),
109    /// [Material Design Icons](https://pictogrammers.com/library/mdi/),
110    /// ...) must set this or the glyph codepoints will render
111    /// as tofu.
112    pub font: Option<iced::Font>,
113    /// The point size to render at, or `None` to let the widget
114    /// pick (currently 14). Themes using larger/smaller glyphs
115    /// than the default can set this to match their intended
116    /// visual balance.
117    pub size: Option<f32>,
118}
119
120impl IconSpec {
121    /// Build a spec from a glyph string. Font and size are `None`
122    /// by default โ€” the widget will render in the iced default
123    /// font at its default size.
124    pub fn new(glyph: impl Into<Cow<'static, str>>) -> Self {
125        Self {
126            glyph: glyph.into(),
127            font: None,
128            size: None,
129        }
130    }
131
132    /// Set the font used to render the glyph. Required for
133    /// icon-font packs where the codepoint is only valid in a
134    /// specific font.
135    pub fn with_font(mut self, font: iced::Font) -> Self {
136        self.font = Some(font);
137        self
138    }
139
140    /// Set the point size. `None` (the default) means the widget
141    /// picks โ€” currently 14.
142    pub fn with_size(mut self, size: f32) -> Self {
143        self.size = Some(size);
144        self
145    }
146}
147
148/// How to render each [`IconRole`] for a given visual design.
149///
150/// Implementers return an [`IconSpec`] per role. The widget calls
151/// [`glyph`](IconTheme::glyph) during view rendering, so the
152/// method should be cheap and pure โ€” build a table at construction
153/// time rather than computing per call if your theme is complex.
154///
155/// `Send + Sync + Debug` because the widget holds the theme in an
156/// `Arc<dyn IconTheme>` and the tree itself is `Debug`-derived.
157///
158/// # Implementing a custom theme
159///
160/// ```
161/// use std::borrow::Cow;
162/// use iced_swdir_tree::{IconRole, IconSpec, IconTheme};
163///
164/// #[derive(Debug)]
165/// struct EmojiTheme;
166///
167/// impl IconTheme for EmojiTheme {
168///     fn glyph(&self, role: IconRole) -> IconSpec {
169///         let s: &'static str = match role {
170///             IconRole::FolderClosed => "๐Ÿ“",
171///             IconRole::FolderOpen => "๐Ÿ“‚",
172///             IconRole::File => "๐Ÿ“„",
173///             IconRole::Error => "โš ",
174///             IconRole::CaretRight => "โ–ธ",
175///             IconRole::CaretDown => "โ–พ",
176///             _ => "?",
177///         };
178///         IconSpec::new(Cow::Borrowed(s))
179///     }
180/// }
181/// ```
182///
183/// Note the `_ =>` arm: [`IconRole`] is `#[non_exhaustive]` so new
184/// variants may be added in future minor releases; always provide
185/// a fallback.
186pub trait IconTheme: Send + Sync + std::fmt::Debug {
187    /// Produce the rendering description for `role`.
188    fn glyph(&self, role: IconRole) -> IconSpec;
189}
190
191/// Stock theme that renders short Unicode symbols available in any
192/// system font. Always available.
193///
194/// This is the default theme when the `icons` feature is disabled,
195/// and serves as a dependency-free fallback that still looks
196/// reasonable out of the box.
197#[derive(Debug, Clone, Copy, Default)]
198pub struct UnicodeTheme;
199
200impl IconTheme for UnicodeTheme {
201    fn glyph(&self, role: IconRole) -> IconSpec {
202        // `IconRole` is `#[non_exhaustive]` but that only affects
203        // *external* crates โ€” inside the defining crate we can (and
204        // must, to silence unreachable-pattern warnings) match every
205        // variant exhaustively. External themes need a `_ =>`
206        // fallback; see the trait's rustdoc.
207        let s: &'static str = match role {
208            IconRole::FolderClosed => "\u{1F4C1}", // ๐Ÿ“
209            IconRole::FolderOpen => "\u{1F4C2}",   // ๐Ÿ“‚
210            IconRole::File => "\u{1F4C4}",         // ๐Ÿ“„
211            IconRole::Error => "\u{26A0}",         // โš 
212            IconRole::CaretRight => "\u{25B8}",    // โ–ธ
213            IconRole::CaretDown => "\u{25BE}",     // โ–พ
214        };
215        IconSpec::new(Cow::Borrowed(s))
216    }
217}
218
219/// Stock theme that renders real [lucide](https://lucide.dev)
220/// vector glyphs.
221///
222/// Available only when the `icons` feature is enabled. The
223/// application is responsible for registering the bundled
224/// [`crate::LUCIDE_FONT_BYTES`] font:
225///
226/// ```ignore
227/// iced::application(App::new, App::update, App::view)
228///     .font(iced_swdir_tree::LUCIDE_FONT_BYTES)
229///     .run()
230/// ```
231///
232/// Without the font registered, lucide codepoints render as tofu
233/// squares โ€” the widget still compiles and the selection/drag/etc.
234/// state all works, the icons just look wrong.
235#[cfg(feature = "icons")]
236#[cfg_attr(docsrs, doc(cfg(feature = "icons")))]
237#[derive(Debug, Clone, Copy, Default)]
238pub struct LucideTheme;
239
240#[cfg(feature = "icons")]
241impl IconTheme for LucideTheme {
242    fn glyph(&self, role: IconRole) -> IconSpec {
243        // lucide-icons exposes an `Icon` enum with codepoints reachable
244        // via `char::from(icon)`. We map each role to the corresponding
245        // enum variant so future lucide updates flow through.
246        use lucide_icons::Icon as LIcon;
247        let lucide_icon: LIcon = match role {
248            IconRole::FolderClosed => LIcon::Folder,
249            IconRole::FolderOpen => LIcon::FolderOpen,
250            IconRole::File => LIcon::File,
251            IconRole::Error => LIcon::AlertCircle,
252            IconRole::CaretRight => LIcon::ChevronRight,
253            IconRole::CaretDown => LIcon::ChevronDown,
254            // `IconRole` is `#[non_exhaustive]` but only externally;
255            // inside this crate every arm must be named. If a new
256            // variant is added, the compile error will point here
257            // and remind whoever adds it to also update the stock
258            // themes. External themes should add a `_ =>` fallback.
259        };
260        let c: char = lucide_icon.into();
261        let mut s = String::with_capacity(c.len_utf8());
262        s.push(c);
263        IconSpec::new(s)
264            .with_font(iced::Font::with_name("lucide"))
265            .with_size(14.0)
266    }
267}
268
269/// Build the default [`IconTheme`] for the current feature set.
270///
271/// * With `icons` feature: [`LucideTheme`].
272/// * Without `icons` feature: [`UnicodeTheme`].
273///
274/// Used internally by [`DirectoryTree::new`](crate::DirectoryTree::new);
275/// applications that want a different default call
276/// [`with_icon_theme`](crate::DirectoryTree::with_icon_theme).
277pub(crate) fn default_theme() -> std::sync::Arc<dyn IconTheme> {
278    #[cfg(feature = "icons")]
279    {
280        std::sync::Arc::new(LucideTheme)
281    }
282    #[cfg(not(feature = "icons"))]
283    {
284        std::sync::Arc::new(UnicodeTheme)
285    }
286}
287
288/// Render a role to an `iced::Element` by consulting `theme`.
289///
290/// This is the one call site view code uses โ€” it takes the theme,
291/// asks for the spec, and produces the element. Keeps feature-flag
292/// and theme-dispatch concerns out of the view layer.
293pub(crate) fn render<'a, Message: 'a>(
294    theme: &dyn IconTheme,
295    role: IconRole,
296) -> Element<'a, Message> {
297    use iced::widget::text;
298    let spec = theme.glyph(role);
299    // Cow -> String ownership: text() wants a `text::IntoFragment`
300    // which accepts owned Strings; build one unconditionally. For
301    // typical `Cow::Borrowed(&'static str)` themes this is one
302    // small allocation per icon per render, which matches the
303    // pre-0.7 cost of `text("๐Ÿ“")` constructing its own text
304    // element.
305    let mut t = text(spec.glyph.into_owned()).size(spec.size.unwrap_or(14.0));
306    if let Some(font) = spec.font {
307        t = t.font(font);
308    }
309    t.into()
310}
311
312#[cfg(test)]
313mod tests;