Skip to main content

beamterm_core/
lib.rs

1pub(crate) mod error;
2pub mod gl;
3mod mat4;
4mod position;
5mod url;
6
7pub use ::beamterm_data::{DebugSpacePattern, FontAtlasData, GlyphEffect};
8pub use beamterm_data::FontStyle;
9pub use error::Error;
10pub use gl::{
11    Atlas, CellData, CellDynamic, CellIterator, CellQuery, Drawable, FontAtlas, GlState, GlyphSlot,
12    GlyphTracker, RenderContext, SelectionMode, SelectionTracker, StaticFontAtlas, TerminalGrid,
13    select,
14};
15pub use position::CursorPosition;
16use unicode_width::UnicodeWidthStr;
17pub use url::{UrlMatch, find_url_at_cursor};
18
19/// GL shader language target for version injection.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum GlslVersion {
22    /// WebGL2 / OpenGL ES 3.0: `#version 300 es`
23    Es300,
24    /// OpenGL 3.3 Core: `#version 330 core`
25    Gl330,
26}
27
28impl GlslVersion {
29    pub fn vertex_preamble(&self) -> &'static str {
30        match self {
31            Self::Es300 => "#version 300 es\nprecision highp float;\n",
32            Self::Gl330 => "#version 330 core\n",
33        }
34    }
35
36    pub fn fragment_preamble(&self) -> &'static str {
37        match self {
38            Self::Es300 => "#version 300 es\nprecision mediump float;\nprecision highp int;\n",
39            Self::Gl330 => "#version 330 core\n",
40        }
41    }
42}
43
44/// Checks if a grapheme is an emoji-presentation-by-default character.
45///
46/// Text-presentation-by-default characters (e.g., `\u{25B6}`, `\u{23ED}`, `\u{23F9}`, `\u{25AA}`) are
47/// recognized by the `emojis` crate but should only be treated as emoji when
48/// explicitly followed by the variation selector `\u{FE0F}`. Without it, they
49/// are regular text glyphs.
50pub fn is_emoji(s: &str) -> bool {
51    match emojis::get(s) {
52        Some(emoji) => {
53            // If the canonical form contains FE0F, the base character is
54            // text-presentation-by-default and should only be emoji when
55            // the caller explicitly includes the variant selector.
56            if emoji.as_str().contains('\u{FE0F}') { s.contains('\u{FE0F}') } else { true }
57        },
58        None => false,
59    }
60}
61
62/// Checks if a grapheme is double-width (emoji or fullwidth character).
63pub fn is_double_width(grapheme: &str) -> bool {
64    grapheme.len() > 1 && (is_emoji(grapheme) || grapheme.width() == 2)
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn test_is_emoji() {
73        // Emoji-presentation-by-default: always emoji
74        assert!(is_emoji("\u{1F680}"));
75        assert!(is_emoji("\u{1F600}"));
76        assert!(is_emoji("\u{23E9}"));
77        assert!(is_emoji("\u{23EA}"));
78
79        // Text-presentation-by-default with FE0F: emoji
80        assert!(is_emoji("\u{25B6}\u{FE0F}"));
81
82        // Text-presentation-by-default without FE0F: NOT emoji
83        assert!(!is_emoji("\u{25B6}"));
84        assert!(!is_emoji("\u{25C0}"));
85        assert!(!is_emoji("\u{23ED}"));
86        assert!(!is_emoji("\u{23F9}"));
87        assert!(!is_emoji("\u{23EE}"));
88        assert!(!is_emoji("\u{25AA}"));
89        assert!(!is_emoji("\u{25AB}"));
90        assert!(!is_emoji("\u{25FC}"));
91
92        // Not recognized by emojis crate at all
93        assert!(!is_emoji("A"));
94        assert!(!is_emoji("\u{2588}"));
95    }
96
97    #[test]
98    fn test_is_double_width() {
99        // emoji-presentation-by-default
100        assert!(is_double_width("\u{1F600}"));
101        assert!(is_double_width(
102            "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}"
103        )); // ZWJ sequence
104
105        [
106            "\u{231A}", "\u{231B}", "\u{23E9}", "\u{23F3}", "\u{2614}", "\u{2615}", "\u{2648}",
107            "\u{2653}", "\u{267F}", "\u{2693}", "\u{26A1}", "\u{26AA}", "\u{26AB}", "\u{26BD}",
108            "\u{26BE}", "\u{26C4}", "\u{26C5}", "\u{26CE}", "\u{26D4}", "\u{26EA}", "\u{26F2}",
109            "\u{26F3}", "\u{26F5}", "\u{26FA}", "\u{26FD}", "\u{25FE}", "\u{2B1B}", "\u{2B1C}",
110            "\u{2B50}", "\u{2B55}", "\u{3030}", "\u{303D}", "\u{3297}", "\u{3299}",
111        ]
112        .iter()
113        .for_each(|s| {
114            assert!(is_double_width(s), "Failed for emoji: {s}");
115        });
116
117        // text-presentation-by-default with FE0F: double-width
118        assert!(is_double_width("\u{25B6}\u{FE0F}"));
119        assert!(is_double_width("\u{25C0}\u{FE0F}"));
120
121        // text-presentation-by-default without FE0F: single-width
122        assert!(!is_double_width("\u{23F8}"));
123        assert!(!is_double_width("\u{23FA}"));
124        assert!(!is_double_width("\u{25AA}"));
125        assert!(!is_double_width("\u{25AB}"));
126        assert!(!is_double_width("\u{25B6}"));
127        assert!(!is_double_width("\u{25C0}"));
128        assert!(!is_double_width("\u{25FB}"));
129        assert!(!is_double_width("\u{2934}"));
130        assert!(!is_double_width("\u{2935}"));
131        assert!(!is_double_width("\u{2B05}"));
132        assert!(!is_double_width("\u{2B07}"));
133        assert!(!is_double_width("\u{26C8}"));
134
135        // CJK
136        assert!(is_double_width("\u{4E2D}"));
137        assert!(is_double_width("\u{65E5}"));
138
139        // single-width
140        assert!(!is_double_width("A"));
141        assert!(!is_double_width("\u{2192}"));
142    }
143
144    #[test]
145    fn test_font_atlas_config_deserialization() {
146        let _ = FontAtlasData::default();
147    }
148}