nandi/jolt-nativepublic Fork 0
3dd441e
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Keep a glyph whole, selector and all, and copy it across whole

`↩️` is an arrow and a variation selector saying to draw it as a picture
rather than as the small mono arrow a terminal draws without one. This
painted a cell a character and spent nothing on the selector, so what
reached the screen was the arrow alone — a reply chip a third the size of
the 🙂 beside it, and one column where the layout had counted two.

A cell now holds the whole glyph: its first character, and the rest where
there is a rest — the selector, the joiners in a family. Splitting text
into glyphs is what measuring, wrapping and painting all do now, so the
three of them agree about a line with an emoji in it.

And a scroll copies cells rather than characters. It paints its content
into a buffer and copies the visible window across, cell by cell through
`set` — which took the character and the style and left behind the rest
of the glyph and the mark that says a cell is the right half of a wide
one. Everything in frq's backlog goes through that copy, which is why the
arrow lost its selector there and nowhere else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-03T00:22:26-07:00 Browse files
3dd441e parent: dce285f
modified crates/jolt-tui/src/layout.rs +4 -4
@@ -10,7 +10,7 @@
1010 //! Nothing here touches a terminal or the screen grid: sizes are arithmetic on
1111 //! the tree, which is why the layout tests below need no TTY.
1212
13-use crate::screen::{char_cols, text_cols, Rect};
13+use crate::screen::{glyph_cols, glyphs, text_cols, Rect};
1414 use crate::tree::{Props, Tag, Tree};
1515
1616 /// How a child that is not filling its cross axis sits in the space it was
@@ -97,13 +97,13 @@ pub fn wrap(text: &str, width: u16) -> Vec<String> {
9797 if word_len > width {
9898 // Longer than the whole line: break it where the line ends
9999 // rather than let it run off the edge.
100- for ch in word.chars() {
101- let cols = char_cols(ch) as usize;
100+ for glyph in glyphs(word) {
101+ let cols = glyph_cols(&glyph) as usize;
102102 if len + cols > width && len > 0 {
103103 lines.push(std::mem::take(&mut line));
104104 len = 0;
105105 }
106- line.push(ch);
106+ line.push_str(&glyph);
107107 len += cols;
108108 }
109109 continue;
@@ -10,7 +10,7 @@
10 //! Nothing here touches a terminal or the screen grid: sizes are arithmetic on10 //! Nothing here touches a terminal or the screen grid: sizes are arithmetic on
11 //! the tree, which is why the layout tests below need no TTY.11 //! the tree, which is why the layout tests below need no TTY.
12 12
13-use crate::screen::{char_cols, text_cols, Rect};13+use crate::screen::{glyph_cols, glyphs, text_cols, Rect};
14 use crate::tree::{Props, Tag, Tree};14 use crate::tree::{Props, Tag, Tree};
15 15
16 /// How a child that is not filling its cross axis sits in the space it was16 /// How a child that is not filling its cross axis sits in the space it was
@@ -97,13 +97,13 @@ pub fn wrap(text: &str, width: u16) -> Vec<String> {
97 if word_len > width {97 if word_len > width {
98 // Longer than the whole line: break it where the line ends98 // Longer than the whole line: break it where the line ends
99 // rather than let it run off the edge.99 // rather than let it run off the edge.
100- for ch in word.chars() {100+ for glyph in glyphs(word) {
101- let cols = char_cols(ch) as usize;101+ let cols = glyph_cols(&glyph) as usize;
102 if len + cols > width && len > 0 {102 if len + cols > width && len > 0 {
103 lines.push(std::mem::take(&mut line));103 lines.push(std::mem::take(&mut line));
104 len = 0;104 len = 0;
105 }105 }
106- line.push(ch);106+ line.push_str(&glyph);
107 len += cols;107 len += cols;
108 }108 }
109 continue;109 continue;
modified crates/jolt-tui/src/paint.rs +1 -1
@@ -400,7 +400,7 @@ impl Painter<'_> {
400400 for y in 0..area.h {
401401 for x in 0..area.w {
402402 if let Some(cell) = buffer.cell(x, y + offset) {
403- self.screen.set(area.x + x, area.y + y, cell.ch, cell.style);
403+ self.screen.put(area.x + x, area.y + y, cell.clone());
404404 }
405405 }
406406 }
@@ -400,7 +400,7 @@ impl Painter<'_> {
400 for y in 0..area.h {400 for y in 0..area.h {
401 for x in 0..area.w {401 for x in 0..area.w {
402 if let Some(cell) = buffer.cell(x, y + offset) {402 if let Some(cell) = buffer.cell(x, y + offset) {
403- self.screen.set(area.x + x, area.y + y, cell.ch, cell.style);403+ self.screen.put(area.x + x, area.y + y, cell.clone());
404 }404 }
405 }405 }
406 }406 }
modified crates/jolt-tui/src/screen.rs +94 -27
@@ -80,26 +80,60 @@ impl Color {
8080 }
8181 }
8282
83-/// How many columns one character takes on screen.
83+/// True for a character that is part of the glyph before it rather than one of
84+/// its own: a variation selector, a zero-width joiner, a skin tone.
85+fn continues_glyph(ch: char) -> bool {
86+ matches!(ch as u32, 0xFE00..=0xFE0F | 0x200D | 0x1F3FB..=0x1F3FF)
87+}
88+
89+/// Split `text` into what a terminal draws as single glyphs.
8490 ///
85-/// Zero for the parts of a glyph that are not drawn — a variation selector, a
86-/// zero-width joiner, a skin tone — two for the emoji a terminal draws double
87-/// width, and one for everything else. This is the whole of what this backend
88-/// knows about character width, and it is enough for what a chat client puts
89-/// on a screen: text, and the emoji in it.
90-pub fn char_cols(ch: char) -> u16 {
91- let c = ch as u32;
92- match c {
93- 0xFE00..=0xFE0F | 0x200D | 0x1F3FB..=0x1F3FF => 0,
91+/// One character is usually one glyph, but an emoji is often several: `↩️` is
92+/// an arrow and a variation selector saying to draw it as a picture, and a
93+/// family is three people and the joiners between them. Cutting between those
94+/// characters is what makes a reply arrow come out as the small mono arrow
95+/// rather than the emoji — the selector says which of the two a terminal
96+/// draws, and it has to travel with the character it is about.
97+pub fn glyphs(text: &str) -> Vec<String> {
98+ let mut out: Vec<String> = Vec::new();
99+ let mut joining = false;
100+ for ch in text.chars() {
101+ let continues = continues_glyph(ch);
102+ if (continues || joining) && !out.is_empty() {
103+ out.last_mut().expect("not empty").push(ch);
104+ } else {
105+ out.push(ch.to_string());
106+ }
107+ // A joiner welds what follows it onto what came before.
108+ joining = ch as u32 == 0x200D;
109+ }
110+ out
111+}
112+
113+/// How many columns one glyph takes on screen.
114+///
115+/// Two for the emoji a terminal draws double width, one for everything else.
116+/// A character out of the emoji blocks is drawn wide on its own; an older
117+/// symbol or dingbat is drawn wide when it carries the selector that asks for
118+/// the picture, and narrow — a character among characters — when it does not.
119+/// This is the whole of what this backend knows about width, and it is enough
120+/// for what a chat client puts on a screen: text, and the emoji in it.
121+pub fn glyph_cols(glyph: &str) -> u16 {
122+ let mut chars = glyph.chars();
123+ let Some(base) = chars.next() else {
124+ return 0;
125+ };
126+ let emoji_presentation = glyph.chars().any(|c| c as u32 == 0xFE0F);
127+ match base as u32 {
94128 0x1F000.. => 2,
95- 0x2600..=0x27BF => 2,
129+ 0x2000..=0x2BFF if emoji_presentation => 2,
96130 _ => 1,
97131 }
98132 }
99133
100134 /// The columns `text` takes, the same way [`Screen::text`] spends them.
101135 pub fn text_cols(text: &str) -> u16 {
102- text.chars().map(char_cols).sum::<u16>().max(0)
136+ glyphs(text).iter().map(|g| glyph_cols(g)).sum()
103137 }
104138
105139 /// The attribute bits a cell can carry. A `u8` rather than a set of `bool`s
@@ -136,9 +170,14 @@ impl Style {
136170 }
137171 }
138172
139-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
173+#[derive(Clone, Debug, PartialEq, Eq)]
140174 pub struct Cell {
141175 pub ch: char,
176+ /// The rest of the glyph, where it took more than one character: the
177+ /// selector that asks for a picture, the joiners in a family. Printed
178+ /// straight after `ch`, which is the only way a terminal draws them as
179+ /// the one glyph they are.
180+ pub tail: Option<Box<str>>,
142181 pub style: Style,
143182 /// The right half of a double-width character. It holds no character of
144183 /// its own: the glyph in the cell to its left is drawn across both, and
@@ -150,6 +189,7 @@ impl Default for Cell {
150189 fn default() -> Self {
151190 Self {
152191 ch: ' ',
192+ tail: None,
153193 style: Style::default(),
154194 trail: false,
155195 }
@@ -242,6 +282,7 @@ impl Screen {
242282 let i = y as usize * self.w as usize + x as usize;
243283 self.cells[i] = Cell {
244284 ch,
285+ tail: None,
245286 style,
246287 trail: false,
247288 };
@@ -254,6 +295,7 @@ impl Screen {
254295 let i = y as usize * self.w as usize + x as usize;
255296 self.cells[i] = Cell {
256297 ch: ' ',
298+ tail: None,
257299 style,
258300 trail: true,
259301 };
@@ -264,26 +306,27 @@ impl Screen {
264306 /// columns were used.
265307 pub fn text(&mut self, x: u16, y: u16, width: u16, text: &str, style: Style) -> u16 {
266308 let mut col = 0u16;
267- for ch in text.chars() {
309+ for glyph in glyphs(text) {
268310 if col >= width {
269311 break;
270312 }
313+ let mut chars = glyph.chars();
314+ let ch = chars.next().unwrap_or(' ');
271315 // A control character in a label would move the cursor; show it as
272316 // a dot instead of letting it rearrange the screen.
273317 let ch = if (ch as u32) < 0x20 { '·' } else { ch };
274- let cols = char_cols(ch);
275- if cols == 0 {
276- // A joiner or a variation selector: part of the glyph before
277- // it, and drawn with it. Keeping it in a cell of its own would
278- // spend a column on something with no picture.
279- continue;
280- }
318+ let cols = glyph_cols(&glyph);
281319 if col + cols > width {
282320 // Half of a wide glyph is a different character, so the last
283321 // column stays blank rather than showing one.
284322 break;
285323 }
286- self.set(x.saturating_add(col), y, ch, style);
324+ let at = x.saturating_add(col);
325+ self.set(at, y, ch, style);
326+ let rest: String = chars.collect();
327+ if !rest.is_empty() {
328+ self.set_tail(at, y, rest);
329+ }
287330 if cols == 2 {
288331 self.set_trail(x.saturating_add(col + 1), y, style);
289332 }
@@ -292,6 +335,25 @@ impl Screen {
292335 col
293336 }
294337
338+ /// Put a whole cell down as it is — what a scroll does when it copies the
339+ /// visible window of its content across. Cell by character would drop the
340+ /// rest of a glyph and the right half of a wide one, which is a reply
341+ /// arrow painted as the small mono arrow and a row a column out.
342+ pub fn put(&mut self, x: u16, y: u16, cell: Cell) {
343+ if x < self.w && y < self.h {
344+ let i = y as usize * self.w as usize + x as usize;
345+ self.cells[i] = cell;
346+ }
347+ }
348+
349+ /// The rest of a glyph, on the cell its first character went in.
350+ fn set_tail(&mut self, x: u16, y: u16, rest: String) {
351+ if x < self.w && y < self.h {
352+ let i = y as usize * self.w as usize + x as usize;
353+ self.cells[i].tail = Some(rest.into_boxed_str());
354+ }
355+ }
356+
295357 /// Paint every cell of `rect` with `style`, keeping the characters — that
296358 /// is what a background is: a colour behind whatever is already there.
297359 pub fn fill(&mut self, rect: Rect, style: Style) {
@@ -316,11 +378,16 @@ impl Screen {
316378 let start = y as usize * self.w as usize;
317379 // Without the trailing halves: they hold no character, and a reader —
318380 // a test, a bug report — wants the line as it looks.
319- let row: String = self.cells[start..start + self.w as usize]
320- .iter()
321- .filter(|c| !c.trail)
322- .map(|c| c.ch)
323- .collect();
381+ let mut row = String::new();
382+ for cell in self.cells[start..start + self.w as usize].iter() {
383+ if cell.trail {
384+ continue;
385+ }
386+ row.push(cell.ch);
387+ if let Some(tail) = &cell.tail {
388+ row.push_str(tail);
389+ }
390+ }
324391 row.trim_end().to_owned()
325392 }
326393
@@ -80,26 +80,60 @@ impl Color {
80 }80 }
81 }81 }
82 82
83-/// How many columns one character takes on screen.83+/// True for a character that is part of the glyph before it rather than one of
84+/// its own: a variation selector, a zero-width joiner, a skin tone.
85+fn continues_glyph(ch: char) -> bool {
86+ matches!(ch as u32, 0xFE00..=0xFE0F | 0x200D | 0x1F3FB..=0x1F3FF)
87+}
88+
89+/// Split `text` into what a terminal draws as single glyphs.
84 ///90 ///
85-/// Zero for the parts of a glyph that are not drawn — a variation selector, a91+/// One character is usually one glyph, but an emoji is often several: `↩️` is
86-/// zero-width joiner, a skin tone — two for the emoji a terminal draws double92+/// an arrow and a variation selector saying to draw it as a picture, and a
87-/// width, and one for everything else. This is the whole of what this backend93+/// family is three people and the joiners between them. Cutting between those
88-/// knows about character width, and it is enough for what a chat client puts94+/// characters is what makes a reply arrow come out as the small mono arrow
89-/// on a screen: text, and the emoji in it.95+/// rather than the emoji — the selector says which of the two a terminal
90-pub fn char_cols(ch: char) -> u16 {96+/// draws, and it has to travel with the character it is about.
91- let c = ch as u32;97+pub fn glyphs(text: &str) -> Vec<String> {
92- match c {98+ let mut out: Vec<String> = Vec::new();
93- 0xFE00..=0xFE0F | 0x200D | 0x1F3FB..=0x1F3FF => 0,99+ let mut joining = false;
100+ for ch in text.chars() {
101+ let continues = continues_glyph(ch);
102+ if (continues || joining) && !out.is_empty() {
103+ out.last_mut().expect("not empty").push(ch);
104+ } else {
105+ out.push(ch.to_string());
106+ }
107+ // A joiner welds what follows it onto what came before.
108+ joining = ch as u32 == 0x200D;
109+ }
110+ out
111+}
112+
113+/// How many columns one glyph takes on screen.
114+///
115+/// Two for the emoji a terminal draws double width, one for everything else.
116+/// A character out of the emoji blocks is drawn wide on its own; an older
117+/// symbol or dingbat is drawn wide when it carries the selector that asks for
118+/// the picture, and narrow — a character among characters — when it does not.
119+/// This is the whole of what this backend knows about width, and it is enough
120+/// for what a chat client puts on a screen: text, and the emoji in it.
121+pub fn glyph_cols(glyph: &str) -> u16 {
122+ let mut chars = glyph.chars();
123+ let Some(base) = chars.next() else {
124+ return 0;
125+ };
126+ let emoji_presentation = glyph.chars().any(|c| c as u32 == 0xFE0F);
127+ match base as u32 {
94 0x1F000.. => 2,128 0x1F000.. => 2,
95- 0x2600..=0x27BF => 2,129+ 0x2000..=0x2BFF if emoji_presentation => 2,
96 _ => 1,130 _ => 1,
97 }131 }
98 }132 }
99 133
100 /// The columns `text` takes, the same way [`Screen::text`] spends them.134 /// The columns `text` takes, the same way [`Screen::text`] spends them.
101 pub fn text_cols(text: &str) -> u16 {135 pub fn text_cols(text: &str) -> u16 {
102- text.chars().map(char_cols).sum::<u16>().max(0)136+ glyphs(text).iter().map(|g| glyph_cols(g)).sum()
103 }137 }
104 138
105 /// The attribute bits a cell can carry. A `u8` rather than a set of `bool`s139 /// The attribute bits a cell can carry. A `u8` rather than a set of `bool`s
@@ -136,9 +170,14 @@ impl Style {
136 }170 }
137 }171 }
138 172
139-#[derive(Clone, Copy, Debug, PartialEq, Eq)]173+#[derive(Clone, Debug, PartialEq, Eq)]
140 pub struct Cell {174 pub struct Cell {
141 pub ch: char,175 pub ch: char,
176+ /// The rest of the glyph, where it took more than one character: the
177+ /// selector that asks for a picture, the joiners in a family. Printed
178+ /// straight after `ch`, which is the only way a terminal draws them as
179+ /// the one glyph they are.
180+ pub tail: Option<Box<str>>,
142 pub style: Style,181 pub style: Style,
143 /// The right half of a double-width character. It holds no character of182 /// The right half of a double-width character. It holds no character of
144 /// its own: the glyph in the cell to its left is drawn across both, and183 /// its own: the glyph in the cell to its left is drawn across both, and
@@ -150,6 +189,7 @@ impl Default for Cell {
150 fn default() -> Self {189 fn default() -> Self {
151 Self {190 Self {
152 ch: ' ',191 ch: ' ',
192+ tail: None,
153 style: Style::default(),193 style: Style::default(),
154 trail: false,194 trail: false,
155 }195 }
@@ -242,6 +282,7 @@ impl Screen {
242 let i = y as usize * self.w as usize + x as usize;282 let i = y as usize * self.w as usize + x as usize;
243 self.cells[i] = Cell {283 self.cells[i] = Cell {
244 ch,284 ch,
285+ tail: None,
245 style,286 style,
246 trail: false,287 trail: false,
247 };288 };
@@ -254,6 +295,7 @@ impl Screen {
254 let i = y as usize * self.w as usize + x as usize;295 let i = y as usize * self.w as usize + x as usize;
255 self.cells[i] = Cell {296 self.cells[i] = Cell {
256 ch: ' ',297 ch: ' ',
298+ tail: None,
257 style,299 style,
258 trail: true,300 trail: true,
259 };301 };
@@ -264,26 +306,27 @@ impl Screen {
264 /// columns were used.306 /// columns were used.
265 pub fn text(&mut self, x: u16, y: u16, width: u16, text: &str, style: Style) -> u16 {307 pub fn text(&mut self, x: u16, y: u16, width: u16, text: &str, style: Style) -> u16 {
266 let mut col = 0u16;308 let mut col = 0u16;
267- for ch in text.chars() {309+ for glyph in glyphs(text) {
268 if col >= width {310 if col >= width {
269 break;311 break;
270 }312 }
313+ let mut chars = glyph.chars();
314+ let ch = chars.next().unwrap_or(' ');
271 // A control character in a label would move the cursor; show it as315 // A control character in a label would move the cursor; show it as
272 // a dot instead of letting it rearrange the screen.316 // a dot instead of letting it rearrange the screen.
273 let ch = if (ch as u32) < 0x20 { '·' } else { ch };317 let ch = if (ch as u32) < 0x20 { '·' } else { ch };
274- let cols = char_cols(ch);318+ let cols = glyph_cols(&glyph);
275- if cols == 0 {
276- // A joiner or a variation selector: part of the glyph before
277- // it, and drawn with it. Keeping it in a cell of its own would
278- // spend a column on something with no picture.
279- continue;
280- }
281 if col + cols > width {319 if col + cols > width {
282 // Half of a wide glyph is a different character, so the last320 // Half of a wide glyph is a different character, so the last
283 // column stays blank rather than showing one.321 // column stays blank rather than showing one.
284 break;322 break;
285 }323 }
286- self.set(x.saturating_add(col), y, ch, style);324+ let at = x.saturating_add(col);
325+ self.set(at, y, ch, style);
326+ let rest: String = chars.collect();
327+ if !rest.is_empty() {
328+ self.set_tail(at, y, rest);
329+ }
287 if cols == 2 {330 if cols == 2 {
288 self.set_trail(x.saturating_add(col + 1), y, style);331 self.set_trail(x.saturating_add(col + 1), y, style);
289 }332 }
@@ -292,6 +335,25 @@ impl Screen {
292 col335 col
293 }336 }
294 337
338+ /// Put a whole cell down as it is — what a scroll does when it copies the
339+ /// visible window of its content across. Cell by character would drop the
340+ /// rest of a glyph and the right half of a wide one, which is a reply
341+ /// arrow painted as the small mono arrow and a row a column out.
342+ pub fn put(&mut self, x: u16, y: u16, cell: Cell) {
343+ if x < self.w && y < self.h {
344+ let i = y as usize * self.w as usize + x as usize;
345+ self.cells[i] = cell;
346+ }
347+ }
348+
349+ /// The rest of a glyph, on the cell its first character went in.
350+ fn set_tail(&mut self, x: u16, y: u16, rest: String) {
351+ if x < self.w && y < self.h {
352+ let i = y as usize * self.w as usize + x as usize;
353+ self.cells[i].tail = Some(rest.into_boxed_str());
354+ }
355+ }
356+
295 /// Paint every cell of `rect` with `style`, keeping the characters — that357 /// Paint every cell of `rect` with `style`, keeping the characters — that
296 /// is what a background is: a colour behind whatever is already there.358 /// is what a background is: a colour behind whatever is already there.
297 pub fn fill(&mut self, rect: Rect, style: Style) {359 pub fn fill(&mut self, rect: Rect, style: Style) {
@@ -316,11 +378,16 @@ impl Screen {
316 let start = y as usize * self.w as usize;378 let start = y as usize * self.w as usize;
317 // Without the trailing halves: they hold no character, and a reader —379 // Without the trailing halves: they hold no character, and a reader —
318 // a test, a bug report — wants the line as it looks.380 // a test, a bug report — wants the line as it looks.
319- let row: String = self.cells[start..start + self.w as usize]381+ let mut row = String::new();
320- .iter()382+ for cell in self.cells[start..start + self.w as usize].iter() {
321- .filter(|c| !c.trail)383+ if cell.trail {
322- .map(|c| c.ch)384+ continue;
323- .collect();385+ }
386+ row.push(cell.ch);
387+ if let Some(tail) = &cell.tail {
388+ row.push_str(tail);
389+ }
390+ }
324 row.trim_end().to_owned()391 row.trim_end().to_owned()
325 }392 }
326 393
modified crates/jolt-tui/src/term.rs +9 -2
@@ -156,8 +156,15 @@ impl Term {
156156 write_style(&mut self.out, cell.style)?;
157157 style = Some(cell.style);
158158 }
159- queue!(self.out, style::Print(cell.ch))?;
160- at = Some((x + screen::char_cols(cell.ch), y));
159+ // The whole glyph, first character and rest: a terminal handed an
160+ // arrow without the selector that follows it draws the small mono
161+ // arrow rather than the emoji.
162+ let mut glyph = cell.ch.to_string();
163+ if let Some(tail) = &cell.tail {
164+ glyph.push_str(tail);
165+ }
166+ queue!(self.out, style::Print(&glyph))?;
167+ at = Some((x + screen::glyph_cols(&glyph), y));
161168 }
162169 queue!(self.out, style::ResetColor)?;
163170 match cursor {
@@ -156,8 +156,15 @@ impl Term {
156 write_style(&mut self.out, cell.style)?;156 write_style(&mut self.out, cell.style)?;
157 style = Some(cell.style);157 style = Some(cell.style);
158 }158 }
159- queue!(self.out, style::Print(cell.ch))?;159+ // The whole glyph, first character and rest: a terminal handed an
160- at = Some((x + screen::char_cols(cell.ch), y));160+ // arrow without the selector that follows it draws the small mono
161+ // arrow rather than the emoji.
162+ let mut glyph = cell.ch.to_string();
163+ if let Some(tail) = &cell.tail {
164+ glyph.push_str(tail);
165+ }
166+ queue!(self.out, style::Print(&glyph))?;
167+ at = Some((x + screen::glyph_cols(&glyph), y));
161 }168 }
162 queue!(self.out, style::ResetColor)?;169 queue!(self.out, style::ResetColor)?;
163 match cursor {170 match cursor {
modified crates/jolt-tui/src/tests.rs +13 -0
@@ -425,6 +425,19 @@ fn a_reaction_paints_its_glyph_and_answers_a_click_on_it() {
425425 );
426426 }
427427
428+#[test]
429+fn a_glyph_keeps_the_selector_that_says_to_draw_it_as_a_picture() {
430+ let mut ui = Ui::new(10, 2);
431+ let root = ui.tree.root();
432+ // The reply chip: an arrow, and a selector asking for the emoji rather
433+ // than the small mono arrow a terminal draws without it.
434+ node(&mut ui, root, "reaction", &[("emoji", "↩️")]);
435+ ui.frame();
436+ assert_eq!(ui.screen.line(0), "\u{fe0f}");
437+ // And it is two columns, like the emoji beside it on the row.
438+ assert_eq!(ui.screen.cell(1, 0).map(|c| c.trail), Some(true));
439+}
440+
428441 #[test]
429442 fn an_emoji_is_two_columns_wide_and_what_follows_it_knows_that() {
430443 let mut ui = Ui::new(12, 3);
@@ -425,6 +425,19 @@ fn a_reaction_paints_its_glyph_and_answers_a_click_on_it() {
425 );425 );
426 }426 }
427 427
428+#[test]
429+fn a_glyph_keeps_the_selector_that_says_to_draw_it_as_a_picture() {
430+ let mut ui = Ui::new(10, 2);
431+ let root = ui.tree.root();
432+ // The reply chip: an arrow, and a selector asking for the emoji rather
433+ // than the small mono arrow a terminal draws without it.
434+ node(&mut ui, root, "reaction", &[("emoji", "↩️")]);
435+ ui.frame();
436+ assert_eq!(ui.screen.line(0), "\u{fe0f}");
437+ // And it is two columns, like the emoji beside it on the row.
438+ assert_eq!(ui.screen.cell(1, 0).map(|c| c.trail), Some(true));
439+}
440+
428 #[test]441 #[test]
429 fn an_emoji_is_two_columns_wide_and_what_follows_it_knows_that() {442 fn an_emoji_is_two_columns_wide_and_what_follows_it_knows_that() {
430 let mut ui = Ui::new(12, 3);443 let mut ui = Ui::new(12, 3);