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;