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/// Aligns the given [`cosmic_text::Buffer`] with the given [`Alignment`]
261/// and returns its minimum [`Size`].
262pub fn align(
263    buffer: &mut cosmic_text::Buffer,
264    font_system: &mut cosmic_text::FontSystem,
265    alignment: Alignment,
266) -> Size {
267    let (min_bounds, has_rtl) = measure(buffer);
268    let mut needs_relayout = has_rtl;
269
270    if let Some(align) = to_align(alignment) {
271        let has_multiple_lines = buffer.lines.len() > 1
272            || buffer.lines.first().is_some_and(|line| {
273                line.layout_opt().is_some_and(|layout| layout.len() > 1)
274            });
275
276        if has_multiple_lines {
277            for line in &mut buffer.lines {
278                let _ = line.set_align(Some(align));
279            }
280
281            needs_relayout = true;
282        } else if let Some(line) = buffer.lines.first_mut() {
283            needs_relayout = line.set_align(None);
284        }
285    }
286
287    // TODO: Avoid relayout with some changes to `cosmic-text` (?)
288    if needs_relayout {
289        log::trace!("Relayouting paragraph...");
290
291        buffer.set_size(
292            font_system,
293            Some(min_bounds.width),
294            Some(min_bounds.height),
295        );
296    }
297
298    min_bounds
299}
300
301/// Returns the attributes of the given [`Font`].
302pub fn to_attributes(font: Font) -> cosmic_text::Attrs<'static> {
303    cosmic_text::Attrs::new()
304        .family(to_family(font.family))
305        .weight(to_weight(font.weight))
306        .stretch(to_stretch(font.stretch))
307        .style(to_style(font.style))
308}
309
310fn to_family(family: font::Family) -> cosmic_text::Family<'static> {
311    match family {
312        font::Family::Name(name) => cosmic_text::Family::Name(name),
313        font::Family::SansSerif => cosmic_text::Family::SansSerif,
314        font::Family::Serif => cosmic_text::Family::Serif,
315        font::Family::Cursive => cosmic_text::Family::Cursive,
316        font::Family::Fantasy => cosmic_text::Family::Fantasy,
317        font::Family::Monospace => cosmic_text::Family::Monospace,
318    }
319}
320
321fn to_weight(weight: font::Weight) -> cosmic_text::Weight {
322    match weight {
323        font::Weight::Thin => cosmic_text::Weight::THIN,
324        font::Weight::ExtraLight => cosmic_text::Weight::EXTRA_LIGHT,
325        font::Weight::Light => cosmic_text::Weight::LIGHT,
326        font::Weight::Normal => cosmic_text::Weight::NORMAL,
327        font::Weight::Medium => cosmic_text::Weight::MEDIUM,
328        font::Weight::Semibold => cosmic_text::Weight::SEMIBOLD,
329        font::Weight::Bold => cosmic_text::Weight::BOLD,
330        font::Weight::ExtraBold => cosmic_text::Weight::EXTRA_BOLD,
331        font::Weight::Black => cosmic_text::Weight::BLACK,
332    }
333}
334
335fn to_stretch(stretch: font::Stretch) -> cosmic_text::Stretch {
336    match stretch {
337        font::Stretch::UltraCondensed => cosmic_text::Stretch::UltraCondensed,
338        font::Stretch::ExtraCondensed => cosmic_text::Stretch::ExtraCondensed,
339        font::Stretch::Condensed => cosmic_text::Stretch::Condensed,
340        font::Stretch::SemiCondensed => cosmic_text::Stretch::SemiCondensed,
341        font::Stretch::Normal => cosmic_text::Stretch::Normal,
342        font::Stretch::SemiExpanded => cosmic_text::Stretch::SemiExpanded,
343        font::Stretch::Expanded => cosmic_text::Stretch::Expanded,
344        font::Stretch::ExtraExpanded => cosmic_text::Stretch::ExtraExpanded,
345        font::Stretch::UltraExpanded => cosmic_text::Stretch::UltraExpanded,
346    }
347}
348
349fn to_style(style: font::Style) -> cosmic_text::Style {
350    match style {
351        font::Style::Normal => cosmic_text::Style::Normal,
352        font::Style::Italic => cosmic_text::Style::Italic,
353        font::Style::Oblique => cosmic_text::Style::Oblique,
354    }
355}
356
357fn to_align(alignment: Alignment) -> Option<cosmic_text::Align> {
358    match alignment {
359        Alignment::Default => None,
360        Alignment::Left => Some(cosmic_text::Align::Left),
361        Alignment::Center => Some(cosmic_text::Align::Center),
362        Alignment::Right => Some(cosmic_text::Align::Right),
363        Alignment::Justified => Some(cosmic_text::Align::Justified),
364    }
365}
366
367/// Converts some [`Shaping`] strategy to a [`cosmic_text::Shaping`] strategy.
368pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping {
369    match shaping {
370        Shaping::Basic => cosmic_text::Shaping::Basic,
371        Shaping::Advanced => cosmic_text::Shaping::Advanced,
372    }
373}
374
375/// Converts some [`Wrapping`] strategy to a [`cosmic_text::Wrap`] strategy.
376pub fn to_wrap(wrapping: Wrapping) -> cosmic_text::Wrap {
377    match wrapping {
378        Wrapping::None => cosmic_text::Wrap::None,
379        Wrapping::Word => cosmic_text::Wrap::Word,
380        Wrapping::Glyph => cosmic_text::Wrap::Glyph,
381        Wrapping::WordOrGlyph => cosmic_text::Wrap::WordOrGlyph,
382    }
383}
384
385/// Converts some [`Color`] to a [`cosmic_text::Color`].
386pub fn to_color(color: Color) -> cosmic_text::Color {
387    let [r, g, b, a] = color.into_rgba8();
388
389    cosmic_text::Color::rgba(r, g, b, a)
390}