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