iced_graphics/
text.rs

1//! Draw text.
2pub mod cache;
3pub mod editor;
4pub mod paragraph;
5
6pub use cache::Cache;
7pub use editor::Editor;
8pub use paragraph::Paragraph;
9
10pub use cosmic_text;
11
12use crate::core::alignment;
13use crate::core::font::{self, Font};
14use crate::core::text::{Alignment, Shaping, Wrapping};
15use crate::core::{Color, Pixels, Point, Rectangle, Size, Transformation};
16
17use std::borrow::Cow;
18use std::collections::HashSet;
19use std::sync::{Arc, OnceLock, RwLock, Weak};
20
21/// A text primitive.
22#[derive(Debug, Clone, PartialEq)]
23pub enum Text {
24    /// A paragraph.
25    #[allow(missing_docs)]
26    Paragraph {
27        paragraph: paragraph::Weak,
28        position: Point,
29        color: Color,
30        clip_bounds: Rectangle,
31        transformation: Transformation,
32    },
33    /// An editor.
34    #[allow(missing_docs)]
35    Editor {
36        editor: editor::Weak,
37        position: Point,
38        color: Color,
39        clip_bounds: Rectangle,
40        transformation: Transformation,
41    },
42    /// Some cached text.
43    Cached {
44        /// The contents of the text.
45        content: String,
46        /// The bounds of the text.
47        bounds: Rectangle,
48        /// The color of the text.
49        color: Color,
50        /// The size of the text in logical pixels.
51        size: Pixels,
52        /// The line height of the text.
53        line_height: Pixels,
54        /// The font of the text.
55        font: Font,
56        /// The horizontal alignment of the text.
57        align_x: Alignment,
58        /// The vertical alignment of the text.
59        align_y: alignment::Vertical,
60        /// The shaping strategy of the text.
61        shaping: Shaping,
62        /// The clip bounds of the text.
63        clip_bounds: Rectangle,
64    },
65    /// Some raw text.
66    #[allow(missing_docs)]
67    Raw {
68        raw: Raw,
69        transformation: Transformation,
70    },
71}
72
73impl Text {
74    /// Returns the visible bounds of the [`Text`].
75    pub fn visible_bounds(&self) -> Option<Rectangle> {
76        let (bounds, align_x, align_y) = match self {
77            Text::Paragraph {
78                position,
79                paragraph,
80                clip_bounds,
81                transformation,
82                ..
83            } => (
84                Rectangle::new(*position, paragraph.min_bounds)
85                    .intersection(clip_bounds)
86                    .map(|bounds| bounds * *transformation),
87                paragraph.align_x,
88                Some(paragraph.align_y),
89            ),
90            Text::Editor {
91                editor,
92                position,
93                clip_bounds,
94                transformation,
95                ..
96            } => (
97                Rectangle::new(*position, editor.bounds)
98                    .intersection(clip_bounds)
99                    .map(|bounds| bounds * *transformation),
100                Alignment::Default,
101                None,
102            ),
103            Text::Cached {
104                bounds,
105                clip_bounds,
106                align_x: horizontal_alignment,
107                align_y: vertical_alignment,
108                ..
109            } => (
110                bounds.intersection(clip_bounds),
111                *horizontal_alignment,
112                Some(*vertical_alignment),
113            ),
114            Text::Raw { raw, .. } => {
115                (Some(raw.clip_bounds), Alignment::Default, None)
116            }
117        };
118
119        let mut bounds = bounds?;
120
121        match align_x {
122            Alignment::Default | Alignment::Left | Alignment::Justified => {}
123            Alignment::Center => {
124                bounds.x -= bounds.width / 2.0;
125            }
126            Alignment::Right => {
127                bounds.x -= bounds.width;
128            }
129        }
130
131        if let Some(alignment) = align_y {
132            match alignment {
133                alignment::Vertical::Top => {}
134                alignment::Vertical::Center => {
135                    bounds.y -= bounds.height / 2.0;
136                }
137                alignment::Vertical::Bottom => {
138                    bounds.y -= bounds.height;
139                }
140            }
141        }
142
143        Some(bounds)
144    }
145}
146
147/// The regular variant of the [Fira Sans] font.
148///
149/// It is loaded as part of the default fonts when the `fira-sans`
150/// feature is enabled.
151///
152/// [Fira Sans]: https://mozilla.github.io/Fira/
153#[cfg(feature = "fira-sans")]
154pub const FIRA_SANS_REGULAR: &[u8] =
155    include_bytes!("../fonts/FiraSans-Regular.ttf").as_slice();
156
157/// Returns the global [`FontSystem`].
158pub fn font_system() -> &'static RwLock<FontSystem> {
159    static FONT_SYSTEM: OnceLock<RwLock<FontSystem>> = OnceLock::new();
160
161    FONT_SYSTEM.get_or_init(|| {
162        RwLock::new(FontSystem {
163            raw: cosmic_text::FontSystem::new_with_fonts([
164                cosmic_text::fontdb::Source::Binary(Arc::new(
165                    include_bytes!("../fonts/Iced-Icons.ttf").as_slice(),
166                )),
167                #[cfg(feature = "fira-sans")]
168                cosmic_text::fontdb::Source::Binary(Arc::new(
169                    include_bytes!("../fonts/FiraSans-Regular.ttf").as_slice(),
170                )),
171            ]),
172            loaded_fonts: HashSet::new(),
173            version: Version::default(),
174        })
175    })
176}
177
178/// A set of system fonts.
179#[allow(missing_debug_implementations)]
180pub struct FontSystem {
181    raw: cosmic_text::FontSystem,
182    loaded_fonts: HashSet<usize>,
183    version: Version,
184}
185
186impl FontSystem {
187    /// Returns the raw [`cosmic_text::FontSystem`].
188    pub fn raw(&mut self) -> &mut cosmic_text::FontSystem {
189        &mut self.raw
190    }
191
192    /// Loads a font from its bytes.
193    pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
194        if let Cow::Borrowed(bytes) = bytes {
195            let address = bytes.as_ptr() as usize;
196
197            if !self.loaded_fonts.insert(address) {
198                return;
199            }
200        }
201
202        let _ = self.raw.db_mut().load_font_source(
203            cosmic_text::fontdb::Source::Binary(Arc::new(bytes.into_owned())),
204        );
205
206        self.version = Version(self.version.0 + 1);
207    }
208
209    /// Returns the current [`Version`] of the [`FontSystem`].
210    ///
211    /// Loading a font will increase the version of a [`FontSystem`].
212    pub fn version(&self) -> Version {
213        self.version
214    }
215}
216
217/// A version number.
218#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
219pub struct Version(u32);
220
221/// A weak reference to a [`cosmic-text::Buffer`] that can be drawn.
222#[derive(Debug, Clone)]
223pub struct Raw {
224    /// A weak reference to a [`cosmic_text::Buffer`].
225    pub buffer: Weak<cosmic_text::Buffer>,
226    /// The position of the text.
227    pub position: Point,
228    /// The color of the text.
229    pub color: Color,
230    /// The clip bounds of the text.
231    pub clip_bounds: Rectangle,
232}
233
234impl PartialEq for Raw {
235    fn eq(&self, _other: &Self) -> bool {
236        // TODO: There is no proper way to compare raw buffers
237        // For now, no two instances of `Raw` text will be equal.
238        // This should be fine, but could trigger unnecessary redraws
239        // in the future.
240        false
241    }
242}
243
244/// Measures the dimensions of the given [`cosmic_text::Buffer`].
245pub fn measure(buffer: &cosmic_text::Buffer) -> (Size, bool) {
246    let (width, height, has_rtl) = buffer.layout_runs().fold(
247        (0.0, 0.0, false),
248        |(width, height, has_rtl), run| {
249            (
250                run.line_w.max(width),
251                height + run.line_height,
252                has_rtl || run.rtl,
253            )
254        },
255    );
256
257    (Size::new(width, height), has_rtl)
258}
259
260/// Returns the attributes of the given [`Font`].
261pub fn to_attributes(font: Font) -> cosmic_text::Attrs<'static> {
262    cosmic_text::Attrs::new()
263        .family(to_family(font.family))
264        .weight(to_weight(font.weight))
265        .stretch(to_stretch(font.stretch))
266        .style(to_style(font.style))
267}
268
269fn to_family(family: font::Family) -> cosmic_text::Family<'static> {
270    match family {
271        font::Family::Name(name) => cosmic_text::Family::Name(name),
272        font::Family::SansSerif => cosmic_text::Family::SansSerif,
273        font::Family::Serif => cosmic_text::Family::Serif,
274        font::Family::Cursive => cosmic_text::Family::Cursive,
275        font::Family::Fantasy => cosmic_text::Family::Fantasy,
276        font::Family::Monospace => cosmic_text::Family::Monospace,
277    }
278}
279
280fn to_weight(weight: font::Weight) -> cosmic_text::Weight {
281    match weight {
282        font::Weight::Thin => cosmic_text::Weight::THIN,
283        font::Weight::ExtraLight => cosmic_text::Weight::EXTRA_LIGHT,
284        font::Weight::Light => cosmic_text::Weight::LIGHT,
285        font::Weight::Normal => cosmic_text::Weight::NORMAL,
286        font::Weight::Medium => cosmic_text::Weight::MEDIUM,
287        font::Weight::Semibold => cosmic_text::Weight::SEMIBOLD,
288        font::Weight::Bold => cosmic_text::Weight::BOLD,
289        font::Weight::ExtraBold => cosmic_text::Weight::EXTRA_BOLD,
290        font::Weight::Black => cosmic_text::Weight::BLACK,
291    }
292}
293
294fn to_stretch(stretch: font::Stretch) -> cosmic_text::Stretch {
295    match stretch {
296        font::Stretch::UltraCondensed => cosmic_text::Stretch::UltraCondensed,
297        font::Stretch::ExtraCondensed => cosmic_text::Stretch::ExtraCondensed,
298        font::Stretch::Condensed => cosmic_text::Stretch::Condensed,
299        font::Stretch::SemiCondensed => cosmic_text::Stretch::SemiCondensed,
300        font::Stretch::Normal => cosmic_text::Stretch::Normal,
301        font::Stretch::SemiExpanded => cosmic_text::Stretch::SemiExpanded,
302        font::Stretch::Expanded => cosmic_text::Stretch::Expanded,
303        font::Stretch::ExtraExpanded => cosmic_text::Stretch::ExtraExpanded,
304        font::Stretch::UltraExpanded => cosmic_text::Stretch::UltraExpanded,
305    }
306}
307
308fn to_style(style: font::Style) -> cosmic_text::Style {
309    match style {
310        font::Style::Normal => cosmic_text::Style::Normal,
311        font::Style::Italic => cosmic_text::Style::Italic,
312        font::Style::Oblique => cosmic_text::Style::Oblique,
313    }
314}
315
316fn to_align(alignment: Alignment) -> Option<cosmic_text::Align> {
317    match alignment {
318        Alignment::Default => None,
319        Alignment::Left => Some(cosmic_text::Align::Left),
320        Alignment::Center => Some(cosmic_text::Align::Center),
321        Alignment::Right => Some(cosmic_text::Align::Right),
322        Alignment::Justified => Some(cosmic_text::Align::Justified),
323    }
324}
325
326/// Converts some [`Shaping`] strategy to a [`cosmic_text::Shaping`] strategy.
327pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping {
328    match shaping {
329        Shaping::Basic => cosmic_text::Shaping::Basic,
330        Shaping::Advanced => cosmic_text::Shaping::Advanced,
331    }
332}
333
334/// Converts some [`Wrapping`] strategy to a [`cosmic_text::Wrap`] strategy.
335pub fn to_wrap(wrapping: Wrapping) -> cosmic_text::Wrap {
336    match wrapping {
337        Wrapping::None => cosmic_text::Wrap::None,
338        Wrapping::Word => cosmic_text::Wrap::Word,
339        Wrapping::Glyph => cosmic_text::Wrap::Glyph,
340        Wrapping::WordOrGlyph => cosmic_text::Wrap::WordOrGlyph,
341    }
342}
343
344/// Converts some [`Color`] to a [`cosmic_text::Color`].
345pub fn to_color(color: Color) -> cosmic_text::Color {
346    let [r, g, b, a] = color.into_rgba8();
347
348    cosmic_text::Color::rgba(r, g, b, a)
349}