1use std::sync::{
2 LazyLock,
3 atomic::{AtomicU64, Ordering},
4};
5
6use omp_core::Str;
7use parking_lot::Mutex;
8use smol_bitmap::SmolBitmap;
9use xutf::{Text, width_char};
10
11static NEXT_FRAME_ID: AtomicU64 = AtomicU64::new(1);
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub struct Size {
16 pub width: u16,
18 pub height: u16,
20}
21
22impl Size {
23 pub const fn new(width: u16, height: u16) -> Self {
25 Self { width, height }
26 }
27
28 fn area(self) -> usize {
29 usize::from(self.width) * usize::from(self.height)
30 }
31}
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
35pub struct Rect {
36 pub x: u16,
38 pub y: u16,
40 pub width: u16,
42 pub height: u16,
44}
45
46impl Rect {
47 pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
49 Self { x, y, width, height }
50 }
51
52 const fn right(self) -> u16 {
53 self.x.saturating_add(self.width)
54 }
55
56 const fn bottom(self) -> u16 {
57 self.y.saturating_add(self.height)
58 }
59}
60
61#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
63pub enum Color {
64 #[default]
66 Default,
67 Indexed(u8),
69 Rgb(u8, u8, u8),
71}
72
73impl Color {
74 pub fn parse(value: &str) -> Option<Self> {
92 use crate::color::CssColor;
93 match CssColor::parse(value)? {
94 CssColor::Rgba(_, _, _, alpha) if alpha <= 0.0 => Some(Self::Default),
95 CssColor::Rgba(red, green, blue, _) => Some(Self::Rgb(red, green, blue)),
96 CssColor::Current => Some(Self::Default),
97 CssColor::System(_) => None,
98 }
99 }
100}
101#[derive(Clone, Copy, Debug, Eq, PartialEq)]
105pub struct Gradient {
106 start: Color,
107 end: Color,
108 angle: u16,
109}
110
111impl Gradient {
112 pub(crate) const fn new(start: Color, end: Color, angle: u16) -> Self {
114 Self { start, end, angle }
115 }
116
117 fn projection(self, bounds: Rect) -> GradientProjection {
118 let (horizontal, vertical) = match self.angle % 360 {
119 0 => (1.0, 0.0),
120 90 => (0.0, 1.0),
121 180 => (-1.0, 0.0),
122 270 => (0.0, -1.0),
123 angle => {
124 let radians = f32::from(angle).to_radians();
125 (radians.cos(), radians.sin())
126 },
127 };
128 let width = f32::from(bounds.width.saturating_sub(1));
129 let height = f32::from(bounds.height.saturating_sub(1));
130 let horizontal_end = horizontal * width;
131 let vertical_end = vertical * height;
132 let min = 0.0_f32
133 .min(horizontal_end)
134 .min(vertical_end)
135 .min(horizontal_end + vertical_end);
136 let max = 0.0_f32
137 .max(horizontal_end)
138 .max(vertical_end)
139 .max(horizontal_end + vertical_end);
140 GradientProjection {
141 start: self.start,
142 end: self.end,
143 horizontal,
144 vertical,
145 origin_x: bounds.x,
146 origin_y: bounds.y,
147 min,
148 span: max - min,
149 }
150 }
151}
152
153#[derive(Clone, Copy)]
154struct GradientProjection {
155 start: Color,
156 end: Color,
157 horizontal: f32,
158 vertical: f32,
159 origin_x: u16,
160 origin_y: u16,
161 min: f32,
162 span: f32,
163}
164
165impl GradientProjection {
166 fn color_at(self, x: u16, y: u16) -> Color {
167 let (Color::Rgb(red, green, blue), Color::Rgb(end_red, end_green, end_blue)) =
168 (self.start, self.end)
169 else {
170 return self.start;
171 };
172 if self.span <= f32::EPSILON {
173 return self.start;
174 }
175 let position = self.vertical.mul_add(
176 f32::from(y) - f32::from(self.origin_y),
177 self.horizontal * (f32::from(x) - f32::from(self.origin_x)),
178 );
179 let amount = ((position - self.min) / self.span).clamp(0.0, 1.0);
180 let channel = |start: u8, end: u8| {
181 f32::mul_add(f32::from(end) - f32::from(start), amount, f32::from(start))
182 .round()
183 .clamp(0.0, 255.0) as u8
184 };
185 Color::Rgb(channel(red, end_red), channel(green, end_green), channel(blue, end_blue))
186 }
187}
188#[derive(Clone, Copy, Debug, Eq, PartialEq)]
194pub struct LinkId(u32);
195impl LinkId {
196 pub(crate) const fn get(self) -> u32 {
197 self.0
198 }
199}
200
201#[derive(Default)]
202struct LinkRegistry {
203 urls: Vec<Str>,
204}
205
206impl LinkRegistry {
207 fn intern(&mut self, url: &str) -> LinkId {
208 if let Some(index) = self.urls.iter().position(|known| known == url) {
209 return LinkId(u32::try_from(index + 1).expect("hyperlink registry exceeds u32"));
210 }
211 self.urls.push(Str::new(url));
212 LinkId(u32::try_from(self.urls.len()).expect("hyperlink registry exceeds u32"))
213 }
214
215 fn get(&self, id: LinkId) -> Option<&str> {
216 let index = usize::try_from(id.0).ok()?.checked_sub(1)?;
217 self.urls.get(index).map(Str::as_str)
218 }
219}
220
221static LINKS: LazyLock<Mutex<LinkRegistry>> = LazyLock::new(|| Mutex::new(LinkRegistry::default()));
222
223pub fn with_link_url<T>(id: LinkId, use_url: impl FnOnce(&str) -> T) -> Option<T> {
224 let links = LINKS.lock();
225 links.get(id).map(use_url)
226}
227
228fn intern_link(url: &str) -> Option<LinkId> {
229 if url.is_empty() || !url.bytes().any(|byte| !matches!(byte, b'\x1b' | b'\x07')) {
230 return None;
231 }
232 let mut links = LINKS.lock();
233 Some(links.intern(url))
234}
235
236#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
238pub struct Style {
239 pub(super) foreground: Color,
240 pub(super) background: Color,
241 pub(super) bold: bool,
242 pub(super) dim: bool,
243 pub(super) italic: bool,
244 pub(super) underline: bool,
245 pub(super) underline_color: Color,
248 pub(super) reverse: bool,
249 pub(super) strikethrough: bool,
250 pub(super) link: Option<LinkId>,
251}
252
253impl Style {
254 pub const fn new() -> Self {
256 Self {
257 foreground: Color::Default,
258 background: Color::Default,
259 bold: false,
260 dim: false,
261 italic: false,
262 underline: false,
263 underline_color: Color::Default,
264 reverse: false,
265 strikethrough: false,
266 link: None,
267 }
268 }
269
270 pub const fn fg(mut self, color: Color) -> Self {
272 self.foreground = color;
273 self
274 }
275
276 pub const fn bg(mut self, color: Color) -> Self {
278 self.background = color;
279 self
280 }
281
282 pub const fn bold(mut self) -> Self {
284 self.bold = true;
285 self
286 }
287
288 pub const fn dim(mut self) -> Self {
290 self.dim = true;
291 self
292 }
293
294 pub const fn italic(mut self) -> Self {
296 self.italic = true;
297 self
298 }
299
300 pub const fn underline(mut self) -> Self {
302 self.underline = true;
303 self
304 }
305
306 pub const fn underline_color(mut self, color: Color) -> Self {
309 self.underline_color = color;
310 self
311 }
312
313 pub const fn reverse(mut self) -> Self {
315 self.reverse = true;
316 self
317 }
318
319 pub const fn strikethrough(mut self) -> Self {
321 self.strikethrough = true;
322 self
323 }
324
325 pub fn link(mut self, url: &str) -> Self {
330 self.link = intern_link(url);
331 self
332 }
333
334 pub(crate) const fn without_link(mut self) -> Self {
335 self.link = None;
336 self
337 }
338
339 pub const fn inherit(mut self, parent: Self) -> Self {
344 if matches!(self.foreground, Color::Default) {
345 self.foreground = parent.foreground;
346 }
347 self.bold |= parent.bold;
348 self.dim |= parent.dim;
349 self.italic |= parent.italic;
350 self.underline |= parent.underline;
351 if matches!(self.underline_color, Color::Default) {
352 self.underline_color = parent.underline_color;
353 }
354 self.reverse |= parent.reverse;
355 self.strikethrough |= parent.strikethrough;
356 if self.link.is_none() {
357 self.link = parent.link;
358 }
359 self
360 }
361
362 pub const fn foreground_color(&self) -> Color {
364 self.foreground
365 }
366
367 pub const fn background_color(&self) -> Color {
369 self.background
370 }
371}
372
373#[derive(Clone, Debug, Eq, PartialEq)]
375pub enum CellContent {
376 Blank,
378 Grapheme {
379 text: Str,
380 width: u16,
381 },
382 Image {
384 id: u32,
385 row: u16,
386 col: u16,
387 rows: u16,
388 cols: u16,
389 },
390 Continuation,
391}
392
393#[derive(Clone, Debug, Eq, PartialEq)]
395pub struct Cell {
396 pub(super) content: CellContent,
397 pub(super) style: Style,
398}
399
400impl Cell {
401 pub(super) const fn blank(style: Style) -> Self {
402 Self { content: CellContent::Blank, style }
403 }
404
405 #[cfg(test)]
406 fn is_default_blank(&self) -> bool {
407 matches!(&self.content, CellContent::Blank) && self.style == Style::default()
408 }
409}
410
411#[derive(Clone, Debug)]
416pub struct Frame {
417 size: Size,
418 cells: Vec<Cell>,
419 cursor: Option<(u16, u16)>,
420 may_have_images: bool,
421 source_id: u64,
422 revision: u64,
423 soft_wraps: SmolBitmap,
426}
427
428impl Frame {
429 pub fn new(size: Size) -> Self {
431 Self {
432 size,
433 cells: vec![Cell::blank(Style::default()); size.area()],
434 cursor: None,
435 may_have_images: false,
436 source_id: NEXT_FRAME_ID.fetch_add(1, Ordering::Relaxed),
437 revision: 0,
438 soft_wraps: SmolBitmap::new(),
439 }
440 }
441
442 pub fn resize_height(&mut self, height: u16, style: Style) {
445 if height == self.size.height {
446 return;
447 }
448 self.touch();
449 let area = usize::from(self.size.width).saturating_mul(usize::from(height));
450 self.cells.resize(area, Cell::blank(style));
451 let first_invalid = usize::from(height.saturating_sub(1));
454 self.soft_wraps.retain(|index| index < first_invalid);
455 self.size.height = height;
456 if self.cursor.is_some_and(|(_, y)| y >= height) {
457 self.cursor = None;
458 }
459 }
460
461 pub fn set_soft_wrap(&mut self, y: u16) {
471 if y.saturating_add(1) >= self.size.height {
472 return;
473 }
474 self.touch();
475 self.soft_wraps.insert(usize::from(y));
476 }
477
478 #[inline]
480 pub fn soft_wrap(&self, y: u16) -> bool {
481 y.saturating_add(1) < self.size.height && self.soft_wraps.get(usize::from(y))
482 }
483
484 #[inline]
485 const fn touch(&mut self) {
486 self.revision = self.revision.wrapping_add(1);
487 }
488
489 #[inline]
490 pub(crate) const fn source_stamp(&self) -> (u64, u64) {
491 (self.source_id, self.revision)
492 }
493
494 #[inline]
495 pub(crate) const fn may_have_images(&self) -> bool {
496 self.may_have_images
497 }
498
499 pub const fn size(&self) -> Size {
501 self.size
502 }
503
504 pub const fn set_cursor(&mut self, x: u16, y: u16) {
509 self.touch();
510 self.cursor = Some((x, y));
511 }
512
513 pub fn clear(&mut self, style: Style) {
515 self.touch();
516 self.cells.fill(Cell::blank(style));
517 self.soft_wraps.clear();
518 }
519
520 pub fn fill(&mut self, rect: Rect, style: Style) {
522 let left = rect.x.min(self.size.width);
523 let right = rect.right().min(self.size.width);
524 let top = rect.y.min(self.size.height);
525 let bottom = rect.bottom().min(self.size.height);
526 if left >= right || top >= bottom {
527 return;
528 }
529 self.touch();
530 self.clear_soft_wraps_touching(top, bottom);
533
534 let blank = Cell::blank(style);
535 for y in top..bottom {
536 self.clear_glyph_at(left, y);
537 if right - left > 1 {
538 self.clear_glyph_at(right - 1, y);
539 }
540 let start = self.index(left, y);
541 let end = self.index(right - 1, y) + 1;
542 self.cells[start..end].fill(blank.clone());
543 }
544 }
545
546 pub fn underlay(&mut self, rect: Rect, color: Color) {
551 self.touch();
552 let right = rect.right().min(self.size.width);
553 let bottom = rect.bottom().min(self.size.height);
554 for y in rect.y.min(self.size.height)..bottom {
555 for x in rect.x.min(self.size.width)..right {
556 let index = self.index(x, y);
557 if self.cells[index].style.background == Color::Default {
558 self.cells[index].style.background = color;
559 }
560 }
561 }
562 }
563
564 pub(crate) fn recolor_fg(&mut self, x: u16, y: u16, recolor: impl FnOnce(Color) -> Color) {
567 if x >= self.size.width || y >= self.size.height {
568 return;
569 }
570 self.touch();
571 let index = self.index(x, y);
572 let style = &mut self.cells[index].style;
573 style.foreground = recolor(style.foreground);
574 }
575
576 pub(crate) fn underlay_gradient(&mut self, rect: Rect, gradient: Gradient, bounds: Rect) {
578 self.touch();
579 let projection = gradient.projection(bounds);
580 let right = rect.right().min(self.size.width);
581 let bottom = rect.bottom().min(self.size.height);
582 for y in rect.y.min(self.size.height)..bottom {
583 let mut x = rect.x.min(self.size.width);
584 while x < right {
585 let index = self.index(x, y);
586 let width = match &self.cells[index].content {
587 CellContent::Blank => 1,
588 CellContent::Grapheme { width, .. } => *width,
589 CellContent::Image { .. } => 1,
590 CellContent::Continuation => {
591 x += 1;
592 continue;
593 },
594 };
595 if self.cells[index].style.background == Color::Default {
596 let color = projection.color_at(x, y);
597 let end = x.saturating_add(width).min(right);
598 for column in x..end {
599 let index = self.index(column, y);
600 if self.cells[index].style.background == Color::Default {
601 self.cells[index].style.background = color;
602 }
603 }
604 }
605 x = x.saturating_add(width.max(1));
606 }
607 }
608 }
609
610 pub(crate) fn gradient_foreground(&mut self, rect: Rect, gradient: Gradient, bounds: Rect) {
612 self.touch();
613 let projection = gradient.projection(bounds);
614 let right = rect.right().min(self.size.width);
615 let bottom = rect.bottom().min(self.size.height);
616 for y in rect.y.min(self.size.height)..bottom {
617 let mut x = rect.x.min(self.size.width);
618 while x < right {
619 let index = self.index(x, y);
620 let (width, visible) = match &self.cells[index].content {
621 CellContent::Blank => (1, false),
622 CellContent::Grapheme { text, width } => (*width, text != " "),
623 CellContent::Image { .. } => (1, true),
624 CellContent::Continuation => {
625 x += 1;
626 continue;
627 },
628 };
629 if visible && self.cells[index].style.foreground == Color::Default {
630 let color = projection.color_at(x, y);
631 let end = x.saturating_add(width).min(right);
632 for column in x..end {
633 let index = self.index(column, y);
634 if self.cells[index].style.foreground == Color::Default {
635 self.cells[index].style.foreground = color;
636 }
637 }
638 }
639 x = x.saturating_add(width.max(1));
640 }
641 }
642 }
643
644 pub fn put_image_cell(
646 &mut self,
647 x: u16,
648 y: u16,
649 id: u32,
650 row: u16,
651 col: u16,
652 rows: u16,
653 cols: u16,
654 ) {
655 if x >= self.size.width || y >= self.size.height {
656 return;
657 }
658 self.touch();
659 self.may_have_images = true;
660 self.clear_glyph_at(x, y);
661 let index = self.index(x, y);
662 self.cells[index] = Cell {
663 content: CellContent::Image { id, row, col, rows, cols },
664 style: Style::default(),
665 };
666 }
667
668 pub fn put(&mut self, x: u16, y: u16, text: &str, style: Style) -> u16 {
673 let width = self.size.width.saturating_sub(x);
674 self.put_clipped(x, y, width, text, style)
675 }
676
677 pub fn put_clipped(&mut self, x: u16, y: u16, width: u16, text: &str, style: Style) -> u16 {
682 if y >= self.size.height {
683 return x;
684 }
685 self.touch();
686
687 let right = x.saturating_add(width).min(self.size.width);
688 let mut column = x;
689 if text.is_ascii() {
690 for &byte in text.as_bytes() {
691 if matches!(byte, b'\n' | b'\r') {
692 break;
693 }
694 if byte.is_ascii_control() {
695 continue;
696 }
697 if column >= right {
698 break;
699 }
700 self.set_ascii(column, y, byte, style);
701 column += 1;
702 }
703 return column;
704 }
705 if let Some(character) = text
706 .chars()
707 .next()
708 .filter(|character| character.len_utf8() == text.len())
709 {
710 if character.is_control() {
711 return column;
712 }
713 let glyph_width = u16::try_from(width_char(character)).unwrap_or(u16::MAX);
714 if glyph_width == 0 || column >= right || glyph_width > right - column {
715 return column;
716 }
717 self.set_grapheme(column, y, text, glyph_width, style);
718 return column + glyph_width;
719 }
720
721 for grapheme in text.graphemes() {
722 if grapheme == "\n" || grapheme == "\r" {
723 break;
724 }
725 if grapheme.chars().any(char::is_control) {
726 continue;
727 }
728
729 let width = u16::try_from(grapheme.visible_width()).unwrap_or(u16::MAX);
730 if width == 0 {
731 continue;
732 }
733 if column >= right || width > right - column {
734 break;
735 }
736
737 self.set_grapheme(column, y, grapheme, width, style);
738 column += width;
739 }
740 column
741 }
742
743 pub(super) const fn cursor(&self) -> Option<(u16, u16)> {
744 self.cursor
745 }
746
747 #[inline(always)]
748 pub(super) fn cell(&self, x: u16, y: u16) -> &Cell {
749 &self.cells[self.index(x, y)]
750 }
751
752 #[inline(always)]
753 pub(super) fn cell_or<'a>(&'a self, row: u16, column: u16, blank: &'a Cell) -> &'a Cell {
754 if row >= self.size.height || column >= self.size.width {
755 blank
756 } else {
757 self.cell(column, row)
758 }
759 }
760
761 pub(crate) fn same_grid(&self, other: &Self) -> bool {
762 self.size == other.size
763 && self.cursor == other.cursor
764 && self.soft_wraps == other.soft_wraps
765 && self.cells == other.cells
766 }
767
768 pub(super) fn row_equals(&self, row: u16, other: &Self, other_row: u16) -> bool {
769 if self.size.width != other.size.width
770 || row >= self.size.height
771 || other_row >= other.size.height
772 {
773 return false;
774 }
775 let width = usize::from(self.size.width);
776 let start = usize::from(row) * width;
777 let other_start = usize::from(other_row) * width;
778 self.cells[start..start + width] == other.cells[other_start..other_start + width]
779 && self.soft_wrap(row) == other.soft_wrap(other_row)
780 }
781
782 pub(crate) fn copy_row_from(&mut self, src: &Self, row: u16) {
786 if row >= self.size.height || row >= src.size.height || self.size.width != src.size.width {
787 return;
788 }
789 self.touch();
790 let width = usize::from(self.size.width);
791 let start = usize::from(row) * width;
792 self.cells[start..start + width].clone_from_slice(&src.cells[start..start + width]);
793 self.may_have_images |= src.may_have_images;
794 }
795
796 pub(crate) fn sync_soft_wraps(&mut self, src: &Self) {
800 if self.soft_wraps != src.soft_wraps {
801 self.touch();
802 self.soft_wraps.clone_from(&src.soft_wraps);
803 }
804 }
805
806 pub fn blit(&mut self, src: &Self, src_top: u16, rows: u16, dst_x: u16, dst_y: u16) {
816 self.touch();
817 let width = src.size.width.min(self.size.width.saturating_sub(dst_x));
818 if let Some((cx, cy)) = src.cursor
819 && cx < width
820 && cy >= src_top
821 && cy < src_top.saturating_add(rows)
822 {
823 let to_y = dst_y.saturating_add(cy - src_top);
824 if to_y < self.size.height {
825 self.cursor = Some((dst_x.saturating_add(cx), to_y));
826 }
827 }
828 let mut copied = 0u16;
829 for row in 0..rows {
830 let from_y = src_top.saturating_add(row);
831 let to_y = dst_y.saturating_add(row);
832 if from_y >= src.size.height || to_y >= self.size.height {
833 break;
834 }
835 copied = row + 1;
836 let mut x = 0u16;
837 while x < width {
838 let cell = src.cell(x, from_y);
839 match &cell.content {
840 CellContent::Blank => {
841 let style = cell.style;
842 self.clear_glyph_at(dst_x + x, to_y);
843 let index = self.index(dst_x + x, to_y);
844 self.cells[index] = Cell::blank(style);
845 x += 1;
846 },
847 CellContent::Grapheme { text, width: glyph_w } => {
848 if x + glyph_w <= width {
849 let text = text.clone();
850 let style = cell.style;
851 let w = *glyph_w;
852 self.set_grapheme(dst_x + x, to_y, &text, w, style);
853 x += w;
854 } else {
855 let style = cell.style;
856 let index = self.index(dst_x + x, to_y);
857 self.clear_glyph_at(dst_x + x, to_y);
858 self.cells[index] = Cell::blank(style);
859 x += 1;
860 }
861 },
862 CellContent::Image { id, row, col, rows, cols } => {
863 self.put_image_cell(dst_x + x, to_y, *id, *row, *col, *rows, *cols);
864 x += 1;
865 },
866 CellContent::Continuation => {
867 let style = cell.style;
869 self.clear_glyph_at(dst_x + x, to_y);
870 let index = self.index(dst_x + x, to_y);
871 self.cells[index] = Cell::blank(style);
872 x += 1;
873 },
874 }
875 }
876 }
877 self.clear_soft_wraps_touching(dst_y, dst_y.saturating_add(copied));
881 if dst_x == 0 && width == self.size.width && width == src.size.width {
882 for offset in 0..copied.saturating_sub(1) {
883 if src.soft_wrap(src_top.saturating_add(offset)) {
884 self.set_soft_wrap(dst_y.saturating_add(offset));
885 }
886 }
887 }
888 }
889
890 fn clear_soft_wraps_touching(&mut self, top: u16, bottom: u16) {
893 for index in usize::from(top.saturating_sub(1))..usize::from(bottom.min(self.size.height)) {
894 self.soft_wraps.set(index, false);
895 }
896 }
897
898 #[inline(always)]
899 fn index(&self, x: u16, y: u16) -> usize {
900 usize::from(y) * usize::from(self.size.width) + usize::from(x)
901 }
902
903 #[inline(always)]
904 fn set_ascii(&mut self, x: u16, y: u16, byte: u8, style: Style) {
905 let lead = self.index(x, y);
906 let existing = &self.cells[lead];
907 if existing.style == style {
908 match &existing.content {
909 CellContent::Blank if byte == b' ' => return,
910 CellContent::Grapheme { text, width: 1 }
911 if text.len() == 1 && text.as_bytes()[0] == byte =>
912 {
913 return;
914 },
915 _ => {},
916 }
917 }
918 if !matches!(
919 &self.cells[lead].content,
920 CellContent::Blank | CellContent::Grapheme { width: 1, .. } | CellContent::Image { .. }
921 ) {
922 self.clear_glyph_at(x, y);
923 }
924 let content = if byte == b' ' {
925 CellContent::Blank
926 } else {
927 let bytes = [byte];
928 let text = unsafe { str::from_utf8_unchecked(&bytes) };
930 CellContent::Grapheme { text: Str::new_inline(text), width: 1 }
931 };
932 self.cells[lead] = Cell { content, style };
933 }
934
935 fn set_grapheme(&mut self, x: u16, y: u16, grapheme: &str, width: u16, style: Style) {
936 if width == 1 {
937 let lead = self.index(x, y);
938 let existing = &self.cells[lead];
939 if existing.style == style {
940 match &existing.content {
941 CellContent::Blank if grapheme == " " => return,
942 CellContent::Grapheme { text, width: 1 } if text == grapheme => return,
943 _ => {},
944 }
945 }
946 if !matches!(
947 &self.cells[lead].content,
948 CellContent::Blank | CellContent::Grapheme { width: 1, .. } | CellContent::Image { .. }
949 ) {
950 self.clear_glyph_at(x, y);
951 }
952 let content = if grapheme == " " {
953 CellContent::Blank
954 } else {
955 CellContent::Grapheme { text: Str::new(grapheme), width }
956 };
957 self.cells[lead] = Cell { content, style };
958 return;
959 }
960
961 for column in x..x + width {
962 self.clear_glyph_at(column, y);
963 }
964
965 let lead = self.index(x, y);
966 self.cells[lead] =
967 Cell { content: CellContent::Grapheme { text: Str::new(grapheme), width }, style };
968 for column in x + 1..x + width {
969 let index = self.index(column, y);
970 self.cells[index] = Cell { content: CellContent::Continuation, style };
971 }
972 }
973
974 fn clear_glyph_at(&mut self, x: u16, y: u16) {
975 let mut start = x;
976 while start > 0 && matches!(self.cell(start, y).content, CellContent::Continuation) {
977 start -= 1;
978 }
979
980 let span = match self.cell(start, y).content {
981 CellContent::Blank => 1,
982 CellContent::Grapheme { width, .. } => width,
983 CellContent::Image { .. } => 1,
984 CellContent::Continuation => 1,
985 };
986 let end = start.saturating_add(span).min(self.size.width);
987 for column in start..end {
988 let index = self.index(column, y);
989 let style = self.cells[index].style;
990 self.cells[index] = Cell::blank(style);
991 }
992 }
993}
994
995#[cfg(test)]
996mod tests {
997 use super::{CellContent, Frame, Rect, Size, Style};
998
999 #[test]
1000 fn overwriting_wide_grapheme_clears_its_continuation() {
1001 let mut frame = Frame::new(Size::new(4, 1));
1002 frame.put(0, 0, "界", Style::default());
1003 frame.put(0, 0, "a", Style::default());
1004
1005 assert!(matches!(frame.cell(1, 0).content, CellContent::Blank));
1006 }
1007
1008 #[test]
1009 fn clipped_wide_grapheme_is_not_drawn() {
1010 let mut frame = Frame::new(Size::new(2, 1));
1011 let end = frame.put(1, 0, "界", Style::default());
1012
1013 assert_eq!(end, 1);
1014 assert!(frame.cell(1, 0).is_default_blank());
1015 }
1016
1017 #[test]
1018 fn clipped_text_preserves_cells_beyond_its_bound() {
1019 let mut frame = Frame::new(Size::new(4, 1));
1020 frame.put(0, 0, "xxxx", Style::default());
1021 let end = frame.put_clipped(1, 0, 2, "ab界", Style::default());
1022
1023 assert_eq!(end, 3);
1024 assert!(matches!(
1025 frame.cell(3, 0).content,
1026 CellContent::Grapheme { ref text, width: 1 } if text == "x"
1027 ));
1028 }
1029
1030 #[test]
1031 fn ascii_text_skips_controls_and_clips_without_unicode_segmentation() {
1032 let mut frame = Frame::new(Size::new(4, 1));
1033 let end = frame.put_clipped(0, 0, 3, "a\tbcd", Style::default());
1034
1035 assert_eq!(end, 3);
1036 assert!(matches!(
1037 frame.cell(2, 0).content,
1038 CellContent::Grapheme { ref text, width: 1 } if text == "c"
1039 ));
1040 assert!(frame.cell(3, 0).is_default_blank());
1041 }
1042 #[test]
1043 fn resizing_height_preserves_rows_and_initializes_growth() {
1044 let mut frame = Frame::new(Size::new(3, 1));
1045 frame.put(0, 0, "a", Style::default());
1046 let fill = Style::new().bold();
1047
1048 frame.resize_height(3, fill);
1049
1050 assert!(matches!(
1051 frame.cell(0, 0).content,
1052 CellContent::Grapheme { ref text, width: 1 } if text == "a"
1053 ));
1054 assert_eq!(frame.cell(0, 2).style, fill);
1055 frame.set_cursor(0, 2);
1056 frame.resize_height(1, Style::default());
1057 assert_eq!(frame.cursor(), None);
1058 }
1059
1060 #[test]
1061 fn soft_wrap_flags_are_layout_metadata() {
1062 let mut frame = Frame::new(Size::new(4, 3));
1063 frame.set_soft_wrap(0);
1064 frame.set_soft_wrap(2);
1065 assert!(frame.soft_wrap(0));
1066 assert!(!frame.soft_wrap(2), "a flag without a following row is ignored");
1067
1068 let mut other = frame.clone();
1069 assert!(frame.row_equals(0, &other, 0));
1070 other.clear(Style::default());
1071 assert!(!other.soft_wrap(0), "clear drops boundary flags");
1072 assert!(!frame.row_equals(0, &other, 0), "row equality includes the boundary bit");
1073
1074 frame.resize_height(1, Style::default());
1075 assert!(!frame.soft_wrap(0), "shrinking drops stale flags");
1076 frame.resize_height(3, Style::default());
1077 assert!(!frame.soft_wrap(0), "regrowth does not resurrect them");
1078 }
1079
1080 #[test]
1081 fn fill_clears_wrap_boundaries_it_touches() {
1082 let mut frame = Frame::new(Size::new(4, 4));
1083 frame.set_soft_wrap(0);
1084 frame.set_soft_wrap(2);
1085 frame.fill(Rect::new(0, 3, 4, 1), Style::default());
1088 assert!(frame.soft_wrap(0));
1089 assert!(!frame.soft_wrap(2));
1090 }
1091
1092 #[test]
1093 fn blit_carries_full_width_wrap_boundaries_and_hardens_partial_copies() {
1094 let mut source = Frame::new(Size::new(4, 3));
1095 source.put(0, 0, "abcd", Style::default());
1096 source.put(0, 1, "ef", Style::default());
1097 source.set_soft_wrap(0);
1098
1099 let mut full = Frame::new(Size::new(4, 4));
1100 full.set_soft_wrap(2);
1101 full.blit(&source, 0, 3, 0, 1);
1102 assert!(full.soft_wrap(1), "a full-width blit carries interior boundaries");
1103 assert!(!full.soft_wrap(2), "boundaries under the copy are replaced, not kept");
1104
1105 let mut partial = Frame::new(Size::new(6, 4));
1106 partial.set_soft_wrap(1);
1107 partial.blit(&source, 0, 3, 1, 1);
1108 assert!(!partial.soft_wrap(1), "an offset copy hardens the rows it rewrites");
1109 }
1110 #[test]
1111 fn blit_translates_cursor_from_copied_region() {
1112 let mut source = Frame::new(Size::new(8, 6));
1113 source.set_cursor(3, 4);
1114 let mut destination = Frame::new(Size::new(12, 8));
1115
1116 destination.blit(&source, 2, 3, 5, 1);
1117
1118 assert_eq!(destination.cursor(), Some((8, 3)));
1119 }
1120
1121 #[test]
1122 fn blit_keeps_cursor_when_source_cursor_cannot_be_copied() {
1123 let mut source = Frame::new(Size::new(8, 6));
1124 let mut destination = Frame::new(Size::new(6, 4));
1125 destination.set_cursor(1, 1);
1126
1127 source.set_cursor(6, 3);
1128 destination.blit(&source, 2, 3, 2, 0);
1129 assert_eq!(destination.cursor(), Some((1, 1)), "cursor past copied width");
1130
1131 source.set_cursor(2, 5);
1132 destination.blit(&source, 2, 3, 0, 0);
1133 assert_eq!(destination.cursor(), Some((1, 1)), "cursor past copied rows");
1134
1135 source.set_cursor(2, 3);
1136 destination.blit(&source, 2, 2, 0, 3);
1137 assert_eq!(destination.cursor(), Some((1, 1)), "translated cursor past destination height");
1138 }
1139}