Skip to content

Commit 8ee2dfb

Browse files
authored
Merge branch 'main' into codex/fix-macos-metal-transparency
2 parents 77557f5 + d4a196a commit 8ee2dfb

10 files changed

Lines changed: 878 additions & 111 deletions

File tree

rio-backend/src/crosswords/mod.rs

Lines changed: 239 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1507,15 +1507,12 @@ impl<U: EventListener> Crosswords<U> {
15071507
..
15081508
}) => {
15091509
for line in (start.row.0..end.row.0).map(Line::from) {
1510-
res += self
1511-
.line_to_string(line, start.col..end.col, start.col.0 != 0)
1512-
.trim_end();
1510+
res +=
1511+
&self.line_to_string(line, start.col..end.col, start.col.0 != 0);
15131512
res += "\n";
15141513
}
15151514

1516-
res += self
1517-
.line_to_string(end.row, start.col..end.col, true)
1518-
.trim_end();
1515+
res += &self.line_to_string(end.row, start.col..end.col, true);
15191516
}
15201517
Some(Selection {
15211518
ty: SelectionType::Lines,
@@ -1532,40 +1529,104 @@ impl<U: EventListener> Crosswords<U> {
15321529
}
15331530

15341531
pub fn bounds_to_string(&self, start: Pos, end: Pos) -> String {
1535-
let mut res = String::new();
1532+
let mut text = String::new();
1533+
let mut blank_rows: usize = 0;
1534+
let mut blank_cells: usize = 0;
1535+
let last_col = self.grid.last_column();
15361536

15371537
for line in (start.row.0..=end.row.0).map(Line::from) {
15381538
let start_col = if line == start.row {
15391539
start.col
15401540
} else {
15411541
Column(0)
15421542
};
1543-
let end_col = if line == end.row {
1544-
end.col
1545-
} else {
1546-
self.grid.last_column()
1547-
};
1543+
let end_col = if line == end.row { end.col } else { last_col };
1544+
1545+
// Carry buffered blank cells across wrap continuations only.
1546+
// Without this, `aaa \n aaa"` (where row N wraps into N+1) would
1547+
// collapse the cross-row gap from two spaces to one.
1548+
let is_wrap_continuation =
1549+
line.0 > start.row.0 && self.grid[line - 1i32][last_col].wrapline();
1550+
if !is_wrap_continuation {
1551+
blank_cells = 0;
1552+
}
1553+
1554+
let mut row_text = String::new();
1555+
let had_content = self.append_cells(
1556+
&mut row_text,
1557+
line,
1558+
start_col..end_col,
1559+
line == end.row,
1560+
&mut blank_cells,
1561+
);
1562+
1563+
if !had_content {
1564+
// Defer entirely-blank rows; trailing blank rows get dropped.
1565+
blank_rows += 1;
1566+
continue;
1567+
}
1568+
1569+
for _ in 0..blank_rows {
1570+
text.push('\n');
1571+
}
1572+
blank_rows = 0;
1573+
1574+
text.push_str(&row_text);
15481575

1549-
res += &self.line_to_string(line, start_col..end_col, line == end.row);
1576+
let cur_wraps = self.grid[line][last_col].wrapline();
1577+
if end_col >= last_col && !cur_wraps {
1578+
text.push('\n');
1579+
blank_cells = 0;
1580+
}
15501581
}
15511582

1552-
res.strip_suffix('\n').map(str::to_owned).unwrap_or(res)
1583+
text.strip_suffix('\n').map(str::to_owned).unwrap_or(text)
15531584
}
15541585

1555-
/// Convert a single line in the grid to a String.
1586+
/// Convert a single line in the grid to a String. Used by Block selection;
1587+
/// trailing blank cells are dropped. No trailing newline is appended —
1588+
/// the caller controls row separation.
15561589
fn line_to_string(
15571590
&self,
15581591
line: Line,
1559-
mut cols: Range<Column>,
1592+
cols: Range<Column>,
15601593
include_wrapped_wide: bool,
15611594
) -> String {
15621595
let mut text = String::new();
1596+
let mut blank_cells = 0;
1597+
self.append_cells(
1598+
&mut text,
1599+
line,
1600+
cols,
1601+
include_wrapped_wide,
1602+
&mut blank_cells,
1603+
);
1604+
text
1605+
}
15631606

1607+
/// Append cells from a single line to `text`, buffering blank cells
1608+
/// (`\0` and trailing spaces) so that:
1609+
/// - `\0` cells inside a run of content become real spaces
1610+
/// - trailing blanks at end of the run are dropped (caller decides
1611+
/// whether to flush them via the `blank_cells` accumulator)
1612+
///
1613+
/// Returns true if the line emitted any non-blank content.
1614+
fn append_cells(
1615+
&self,
1616+
text: &mut String,
1617+
line: Line,
1618+
mut cols: Range<Column>,
1619+
include_wrapped_wide: bool,
1620+
blank_cells: &mut usize,
1621+
) -> bool {
1622+
let mut had_content = false;
15641623
let grid_line = &self.grid[line];
15651624
let line_length = std::cmp::min(grid_line.line_length(), cols.end + 1);
15661625

15671626
// Include wide char when trailing spacer is selected.
1568-
if matches!(grid_line[cols.start].wide(), Wide::Spacer) {
1627+
if cols.start < self.grid.columns()
1628+
&& matches!(grid_line[cols.start].wide(), Wide::Spacer)
1629+
{
15691630
cols.start -= 1;
15701631
}
15711632

@@ -1586,25 +1647,34 @@ impl<U: EventListener> Crosswords<U> {
15861647
tab_mode = true;
15871648
}
15881649

1589-
if !matches!(cell.wide(), Wide::Spacer | Wide::LeadingSpacer) {
1590-
// Push cells primary character.
1591-
text.push(cell.c());
1650+
if matches!(cell.wide(), Wide::Spacer | Wide::LeadingSpacer) {
1651+
continue;
1652+
}
1653+
1654+
let c = cell.c();
1655+
let has_extras = cell.extras_id().is_some();
15921656

1593-
// Push zero-width characters.
1594-
if let Some(extras_id) = cell.extras_id() {
1595-
if let Some(extras) = self.grid.extras_table.get(extras_id) {
1596-
for c in &extras.zerowidth {
1597-
text.push(*c);
1598-
}
1657+
// Buffer blank cells. They only get emitted as real spaces if a
1658+
// non-blank cell follows (on this row or a wrap continuation).
1659+
if !has_extras && (c == '\0' || c == ' ') {
1660+
*blank_cells += 1;
1661+
continue;
1662+
}
1663+
1664+
for _ in 0..*blank_cells {
1665+
text.push(' ');
1666+
}
1667+
*blank_cells = 0;
1668+
1669+
text.push(c);
1670+
if let Some(extras_id) = cell.extras_id() {
1671+
if let Some(extras) = self.grid.extras_table.get(extras_id) {
1672+
for c in &extras.zerowidth {
1673+
text.push(*c);
15991674
}
16001675
}
16011676
}
1602-
}
1603-
1604-
if cols.end >= self.grid.columns() - 1
1605-
&& (line_length.0 == 0 || !self.grid[line][line_length - 1].wrapline())
1606-
{
1607-
text.push('\n');
1677+
had_content = true;
16081678
}
16091679

16101680
// If wide char is not part of the selection, but leading spacer is, include it.
@@ -1613,10 +1683,15 @@ impl<U: EventListener> Crosswords<U> {
16131683
&& matches!(grid_line[line_length - 1].wide(), Wide::LeadingSpacer)
16141684
&& include_wrapped_wide
16151685
{
1686+
for _ in 0..*blank_cells {
1687+
text.push(' ');
1688+
}
1689+
*blank_cells = 0;
16161690
text.push(self.grid[line - 1i32][Column(0)].c());
1691+
had_content = true;
16171692
}
16181693

1619-
text
1694+
had_content
16201695
}
16211696

16221697
#[inline]
@@ -4695,9 +4770,12 @@ mod tests {
46954770
Side::Right,
46964771
);
46974772
}
4773+
// Trailing space on the wrapped row is preserved as a buffered blank
4774+
// and only flushed if a non-blank cell follows on the continuation
4775+
// row. Here the selection ends mid-wrap so the trailing space is dropped.
46984776
assert_eq!(
46994777
term.selection_to_string(),
4700-
Some(String::from("\"aaa\"\n\n aaa "))
4778+
Some(String::from("\"aaa\"\n\n aaa"))
47014779
);
47024780

47034781
// A wrapline.
@@ -4836,6 +4914,133 @@ mod tests {
48364914
);
48374915
}
48384916

4917+
fn make_term_for_selection(rows: usize, cols: usize) -> Crosswords<VoidListener> {
4918+
let size = CrosswordsSize::new(cols, rows);
4919+
let window_id = crate::event::WindowId::from(0);
4920+
Crosswords::new(
4921+
size,
4922+
CursorShape::Block,
4923+
VoidListener {},
4924+
window_id,
4925+
0,
4926+
10_000,
4927+
)
4928+
}
4929+
4930+
fn select_simple(
4931+
term: &mut Crosswords<VoidListener>,
4932+
start: (i32, usize),
4933+
end: (i32, usize),
4934+
) {
4935+
term.selection = Some(Selection::new(
4936+
SelectionType::Simple,
4937+
Pos {
4938+
row: Line(start.0),
4939+
col: Column(start.1),
4940+
},
4941+
Side::Left,
4942+
));
4943+
if let Some(s) = term.selection.as_mut() {
4944+
s.update(
4945+
Pos {
4946+
row: Line(end.0),
4947+
col: Column(end.1),
4948+
},
4949+
Side::Right,
4950+
);
4951+
}
4952+
}
4953+
4954+
/// `\0` cells in the middle of a run of content must be emitted as ASCII
4955+
/// spaces, not raw NULs. This is the "TUI redrew its UI and left holes"
4956+
/// case (e.g. fullscreen apps that paint with cursor positioning).
4957+
#[test]
4958+
fn null_cells_inside_run_become_spaces() {
4959+
let mut term = make_term_for_selection(1, 7);
4960+
let grid = &mut term.grid;
4961+
// Row layout: a, a, \0, \0, \0, b, b
4962+
grid[Line(0)][Column(0)].set_c('a');
4963+
grid[Line(0)][Column(1)].set_c('a');
4964+
grid[Line(0)][Column(5)].set_c('b');
4965+
grid[Line(0)][Column(6)].set_c('b');
4966+
4967+
select_simple(&mut term, (0, 0), (0, 6));
4968+
let s = term.selection_to_string().unwrap();
4969+
assert_eq!(s, "aa bb");
4970+
assert!(!s.contains('\0'), "selection must not contain raw NULs");
4971+
}
4972+
4973+
/// Trailing `\0` and trailing spaces on a non-wrapped row must be dropped
4974+
/// rather than padding the copy out to column width.
4975+
#[test]
4976+
fn trailing_blanks_on_non_wrapped_row_are_dropped() {
4977+
let mut term = make_term_for_selection(1, 8);
4978+
let grid = &mut term.grid;
4979+
grid[Line(0)][Column(0)].set_c('h');
4980+
grid[Line(0)][Column(1)].set_c('i');
4981+
grid[Line(0)][Column(2)].set_c(' ');
4982+
grid[Line(0)][Column(3)].set_c(' ');
4983+
// cols 4..=7 stay as \0
4984+
4985+
select_simple(&mut term, (0, 0), (0, 7));
4986+
assert_eq!(term.selection_to_string(), Some(String::from("hi")));
4987+
}
4988+
4989+
/// Trailing blank rows in a multi-row selection must be dropped, not
4990+
/// emitted as a run of `\n`s.
4991+
#[test]
4992+
fn trailing_blank_rows_are_dropped() {
4993+
let mut term = make_term_for_selection(5, 5);
4994+
let grid = &mut term.grid;
4995+
grid[Line(0)][Column(0)].set_c('x');
4996+
grid[Line(0)][Column(1)].set_c('y');
4997+
// Rows 1..=4 are entirely \0.
4998+
4999+
select_simple(&mut term, (0, 0), (4, 4));
5000+
assert_eq!(term.selection_to_string(), Some(String::from("xy")));
5001+
}
5002+
5003+
/// Blank rows between non-blank rows must still be emitted as `\n`s, so
5004+
/// real visual gaps in the selection are preserved.
5005+
#[test]
5006+
fn blank_rows_between_content_are_preserved() {
5007+
let mut term = make_term_for_selection(5, 5);
5008+
let grid = &mut term.grid;
5009+
grid[Line(0)][Column(0)].set_c('a');
5010+
// Rows 1, 2 entirely \0.
5011+
grid[Line(3)][Column(0)].set_c('b');
5012+
// Row 4 entirely \0.
5013+
5014+
select_simple(&mut term, (0, 0), (4, 4));
5015+
assert_eq!(term.selection_to_string(), Some(String::from("a\n\n\nb")));
5016+
}
5017+
5018+
/// When a row wraps into the next, the trailing-space buffer must carry
5019+
/// across so the visual gap survives the round-trip through the clipboard.
5020+
#[test]
5021+
fn trailing_space_carries_across_wrap_continuation() {
5022+
let mut term = make_term_for_selection(2, 5);
5023+
let grid = &mut term.grid;
5024+
// Row 0: "ab " with a wrap into row 1.
5025+
grid[Line(0)][Column(0)].set_c('a');
5026+
grid[Line(0)][Column(1)].set_c('b');
5027+
grid[Line(0)][Column(2)].set_c(' ');
5028+
grid[Line(0)][Column(3)].set_c(' ');
5029+
grid[Line(0)][Column(4)].set_c(' ');
5030+
grid[Line(0)][Column(4)].set_wrapline(true);
5031+
// Row 1: " cd "
5032+
grid[Line(1)][Column(0)].set_c(' ');
5033+
grid[Line(1)][Column(1)].set_c('c');
5034+
grid[Line(1)][Column(2)].set_c('d');
5035+
grid[Line(1)][Column(3)].set_c(' ');
5036+
grid[Line(1)][Column(4)].set_c(' ');
5037+
5038+
// Trailing spaces on row 1 dropped (no further continuation), but the
5039+
// 4 spaces between `b` and `c` must survive across the wrap.
5040+
select_simple(&mut term, (0, 0), (1, 4));
5041+
assert_eq!(term.selection_to_string(), Some(String::from("ab cd")));
5042+
}
5043+
48395044
#[test]
48405045
fn parse_cargo_version() {
48415046
assert_eq!(version_number("0.0.1-nightly"), 1);

sugarloaf/src/font/constants.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ pub const FONT_CASCADIAMONO_LIGHT: &[u8] =
4242
pub const FONT_CASCADIAMONO_LIGHT_ITALIC: &[u8] =
4343
font!("./resources/CascadiaCode/CascadiaCode-LightItalic.otf");
4444

45-
pub const FONT_CASCADIAMONO_REGULAR: &[u8] =
46-
font!("./resources/CascadiaCode/CascadiaCode-Regular.otf");
45+
pub const FONT_CASCADIAMONO_NF_REGULAR: &[u8] =
46+
font!("./resources/CascadiaCode/CascadiaCodeNF-Regular.otf");
4747

4848
pub const FONT_CASCADIAMONO_SEMI_BOLD: &[u8] =
4949
font!("./resources/CascadiaCode/CascadiaCode-SemiBold.otf");
@@ -57,8 +57,8 @@ pub const FONT_CASCADIAMONO_SEMI_LIGHT: &[u8] =
5757
pub const FONT_CASCADIAMONO_SEMI_LIGHT_ITALIC: &[u8] =
5858
font!("./resources/CascadiaCode/CascadiaCode-SemiLightItalic.otf");
5959

60-
pub const FONT_SYMBOLS_NERD_FONT_MONO: &[u8] =
61-
font!("./resources/SymbolsNerdFontMono/SymbolsNerdFontMono-Regular.ttf");
60+
// pub const FONT_SYMBOLS_NERD_FONT_MONO: &[u8] =
61+
// font!("./resources/SymbolsNerdFontMono/SymbolsNerdFontMono-Regular.ttf");
6262

6363
// macOS gets system Apple Color Emoji through the font fallback chain (see
6464
// `fallbacks::external_fallbacks`) and doesn't need the bundled Twemoji —

0 commit comments

Comments
 (0)