feat(terminal): port page memory model (partial PageList)
First cut of the terminal core memory model from ZIG/src/terminal: size, point, style, hyperlink, ref_counted_set, page and PageList. Design (see docs/rewrite/ARCHITECTURE.md decisions 2-4): - Cell/Row are 8-byte u64 wrappers with Zig's exact bit offsets. - Page owns row/cell Vecs plus style set, grapheme and hyperlink maps; Page::layout reproduces Ghostty's byte-size formula on 64-bit Linux so scrollback-limit pruning matches. - PageList keeps nodes in a generation-checked slab with a free list and list-owned tracked pins addressed by handles. This is explicitly partial (docs/rewrite/notes/ghostty-terminal- pagelist.md): 151/202 PageList tests ported; reflow rebuilds a snapshot instead of Zig's incremental ReflowCursor; point<->pin conversion walks from the list head (no active/viewport caches); RefCountedSet doesn't reproduce probe-dependent ID reuse. A follow-up pass finishes parity. Test Plan: - cargo test -p ghostty-terminal (239 tests)
This commit is contained in:
1 parent
59bc84fde0
commit
b9ac54e794
11 files changed
+8817
-3
No files matched your search
@@ -0,0 +1,2216 @@
|
||||
//! Compact cells and rows, with bounded owned page storage.
|
||||
use crate::{
|
||||
hyperlink::{self, Hyperlink, HyperlinkId},
|
||||
ref_counted_set::{AddError, Layout as SetLayout},
|
||||
style::{self, Style},
|
||||
};
|
||||
use ghostty_vt::color::Rgb;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
/// Default grapheme allocation request, in bytes.
|
||||
pub const GRAPHEME_BYTES_DEFAULT: u32 = 1024;
|
||||
/// Default string allocation request, in bytes.
|
||||
pub const STRING_BYTES_DEFAULT: u32 = 2048;
|
||||
/// Reference ABI size of a hyperlink set item.
|
||||
pub const HYPERLINK_ITEM_SIZE: usize = 48;
|
||||
/// Reference ABI size of a style set item.
|
||||
pub const STYLE_ITEM_SIZE: usize = 36;
|
||||
/// The standard page capacity (smaller grapheme region in tests, as in Zig).
|
||||
pub const STD_CAPACITY: Capacity = Capacity {
|
||||
cols: 215,
|
||||
rows: 215,
|
||||
styles: 128,
|
||||
hyperlink_bytes: 192,
|
||||
grapheme_bytes: if cfg!(test) { 512 } else { 8192 },
|
||||
string_bytes: STRING_BYTES_DEFAULT,
|
||||
};
|
||||
/// ContentTag field of the packed grid representation.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum ContentTag {
|
||||
/// Single codepoint.
|
||||
#[default]
|
||||
Codepoint = 0,
|
||||
/// First codepoint of a grapheme cluster.
|
||||
CodepointGrapheme = 1,
|
||||
/// Background-only palette color.
|
||||
BgColorPalette = 2,
|
||||
/// Background-only RGB color.
|
||||
BgColorRgb = 3,
|
||||
}
|
||||
/// Wide field of the packed grid representation.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum Wide {
|
||||
/// Single grid cell.
|
||||
#[default]
|
||||
Narrow = 0,
|
||||
/// Two-cell character.
|
||||
Wide = 1,
|
||||
/// Trailing half of a wide character.
|
||||
SpacerTail = 2,
|
||||
/// End-of-line spacer before a wrapped wide character.
|
||||
SpacerHead = 3,
|
||||
}
|
||||
/// SemanticContent field of the packed grid representation.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum SemanticContent {
|
||||
/// Ordinary command output.
|
||||
#[default]
|
||||
Output = 0,
|
||||
/// User input.
|
||||
Input = 1,
|
||||
/// Application prompt.
|
||||
Prompt = 2,
|
||||
}
|
||||
/// SemanticPrompt field of the packed grid representation.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum SemanticPrompt {
|
||||
/// No prompt cells.
|
||||
#[default]
|
||||
None = 0,
|
||||
/// Primary prompt row.
|
||||
Prompt = 1,
|
||||
/// Continuation prompt row.
|
||||
PromptContinuation = 2,
|
||||
}
|
||||
/// Eight-byte terminal cell; all-zero bits describe an empty cell.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(transparent)]
|
||||
pub struct Cell(u64);
|
||||
impl Cell {
|
||||
/// Construct an unstyled single-codepoint cell.
|
||||
pub fn init(cp: u32) -> Self {
|
||||
let mut c = Self::default();
|
||||
c.set_codepoint(cp);
|
||||
c
|
||||
}
|
||||
fn bits(self, shift: u32, mask: u64) -> u64 {
|
||||
(self.0 >> shift) & mask
|
||||
}
|
||||
fn set_bits(&mut self, shift: u32, mask: u64, value: u64) {
|
||||
self.0 = (self.0 & !(mask << shift)) | ((value & mask) << shift);
|
||||
}
|
||||
/// Return the bit-packed representation.
|
||||
pub const fn raw(self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
/// True when every bit is zero.
|
||||
pub fn is_zero(self) -> bool {
|
||||
self.0 == 0
|
||||
}
|
||||
/// Return the codepoint, or zero for background-only cells.
|
||||
pub fn codepoint(self) -> u32 {
|
||||
if (self.content_tag() as u8) < 2 {
|
||||
self.bits(2, 0x1fffff) as u32
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
/// Set the codepoint payload without changing the content tag.
|
||||
pub fn set_codepoint(&mut self, cp: u32) {
|
||||
self.set_bits(2, 0xffffff, u64::from(cp & 0x1fffff));
|
||||
}
|
||||
/// Return the palette payload.
|
||||
pub fn color_palette(self) -> u8 {
|
||||
self.bits(2, 255) as u8
|
||||
}
|
||||
/// Set the palette payload without changing the content tag.
|
||||
pub fn set_color_palette(&mut self, v: u8) {
|
||||
self.set_bits(2, 0xffffff, u64::from(v));
|
||||
}
|
||||
/// Return the RGB payload.
|
||||
pub fn color_rgb(self) -> Rgb {
|
||||
Rgb {
|
||||
r: self.bits(2, 255) as u8,
|
||||
g: self.bits(10, 255) as u8,
|
||||
b: self.bits(18, 255) as u8,
|
||||
}
|
||||
}
|
||||
/// Set the RGB payload without changing the content tag.
|
||||
pub fn set_color_rgb(&mut self, v: Rgb) {
|
||||
self.set_bits(
|
||||
2,
|
||||
0xffffff,
|
||||
u64::from(v.r) | (u64::from(v.g) << 8) | (u64::from(v.b) << 16),
|
||||
);
|
||||
}
|
||||
/// True when the cell contains a nonzero text codepoint.
|
||||
pub fn has_text(self) -> bool {
|
||||
self.codepoint() != 0
|
||||
}
|
||||
/// True when the cell refers to extra grapheme codepoints.
|
||||
pub fn has_grapheme(self) -> bool {
|
||||
self.content_tag() == ContentTag::CodepointGrapheme
|
||||
}
|
||||
/// Width in grid cells, including one-cell spacers.
|
||||
pub fn grid_width(self) -> u8 {
|
||||
if self.wide() == Wide::Wide { 2 } else { 1 }
|
||||
}
|
||||
/// True when the cell has a nondefault style ID.
|
||||
pub fn has_styling(self) -> bool {
|
||||
self.style_id() != 0
|
||||
}
|
||||
/// True for a blank narrow textual cell; style does not affect this test.
|
||||
pub fn is_empty(self) -> bool {
|
||||
(self.content_tag() as u8) < 2 && !self.has_text() && self.wide() == Wide::Narrow
|
||||
}
|
||||
/// True if any cell has text.
|
||||
pub fn has_text_any(cells: &[Self]) -> bool {
|
||||
cells.iter().any(|c| c.has_text())
|
||||
}
|
||||
/// Read the content_tag field.
|
||||
pub fn content_tag(self) -> ContentTag {
|
||||
match self.bits(0, 3) {
|
||||
0 => ContentTag::Codepoint,
|
||||
1 => ContentTag::CodepointGrapheme,
|
||||
2 => ContentTag::BgColorPalette,
|
||||
_ => ContentTag::BgColorRgb,
|
||||
}
|
||||
}
|
||||
/// Update the content_tag field.
|
||||
pub fn set_content_tag(&mut self, v: ContentTag) {
|
||||
self.set_bits(0, 3, v as u64);
|
||||
}
|
||||
/// Read the style_id field.
|
||||
pub fn style_id(self) -> u16 {
|
||||
self.bits(26, 65535) as u16
|
||||
}
|
||||
/// Update the style_id field.
|
||||
pub fn set_style_id(&mut self, v: u16) {
|
||||
self.set_bits(26, 65535, v as u64);
|
||||
}
|
||||
/// Read the wide field.
|
||||
pub fn wide(self) -> Wide {
|
||||
match self.bits(42, 3) {
|
||||
0 => Wide::Narrow,
|
||||
1 => Wide::Wide,
|
||||
2 => Wide::SpacerTail,
|
||||
_ => Wide::SpacerHead,
|
||||
}
|
||||
}
|
||||
/// Update the wide field.
|
||||
pub fn set_wide(&mut self, v: Wide) {
|
||||
self.set_bits(42, 3, v as u64);
|
||||
}
|
||||
/// Read the protected field.
|
||||
pub fn protected(self) -> bool {
|
||||
self.bits(44, 1) != 0
|
||||
}
|
||||
/// Update the protected field.
|
||||
pub fn set_protected(&mut self, v: bool) {
|
||||
self.set_bits(44, 1, v as u64);
|
||||
}
|
||||
/// Read the hyperlink field.
|
||||
pub fn hyperlink(self) -> bool {
|
||||
self.bits(45, 1) != 0
|
||||
}
|
||||
/// Update the hyperlink field.
|
||||
pub fn set_hyperlink(&mut self, v: bool) {
|
||||
self.set_bits(45, 1, v as u64);
|
||||
}
|
||||
/// Read the semantic_content field.
|
||||
pub fn semantic_content(self) -> SemanticContent {
|
||||
match self.bits(46, 3) {
|
||||
0 => SemanticContent::Output,
|
||||
1 => SemanticContent::Input,
|
||||
_ => SemanticContent::Prompt,
|
||||
}
|
||||
}
|
||||
/// Update the semantic_content field.
|
||||
pub fn set_semantic_content(&mut self, v: SemanticContent) {
|
||||
self.set_bits(46, 3, v as u64);
|
||||
}
|
||||
}
|
||||
/// Eight-byte row metadata. The low 32 bits hold its first cell index.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(transparent)]
|
||||
pub struct Row(u64);
|
||||
impl Row {
|
||||
/// First cell index in the page's row-major storage.
|
||||
pub fn cells(self) -> usize {
|
||||
(self.0 & 0xffffffff) as usize
|
||||
}
|
||||
/// Set the row's first cell index.
|
||||
pub fn set_cells(&mut self, v: usize) {
|
||||
self.0 = (self.0 & !0xffffffff) | (v as u64 & 0xffffffff);
|
||||
}
|
||||
/// True if cells can have managed style, hyperlink, or grapheme data.
|
||||
pub fn managed_memory(self) -> bool {
|
||||
self.styled() || self.hyperlink() || self.grapheme()
|
||||
}
|
||||
/// Prompt-row classification.
|
||||
pub fn semantic_prompt(self) -> SemanticPrompt {
|
||||
match (self.0 >> 37) & 3 {
|
||||
1 => SemanticPrompt::Prompt,
|
||||
2 => SemanticPrompt::PromptContinuation,
|
||||
_ => SemanticPrompt::None,
|
||||
}
|
||||
}
|
||||
/// Set prompt-row classification.
|
||||
pub fn set_semantic_prompt(&mut self, v: SemanticPrompt) {
|
||||
self.0 = (self.0 & !(3 << 37)) | ((v as u64) << 37);
|
||||
}
|
||||
/// Read the wrap flag.
|
||||
pub fn wrap(self) -> bool {
|
||||
self.0 & (1 << 32) != 0
|
||||
}
|
||||
/// Update the wrap flag.
|
||||
pub fn set_wrap(&mut self, v: bool) {
|
||||
self.0 = (self.0 & !(1 << 32)) | (u64::from(v) << 32);
|
||||
}
|
||||
/// Read the wrap_continuation flag.
|
||||
pub fn wrap_continuation(self) -> bool {
|
||||
self.0 & (1 << 33) != 0
|
||||
}
|
||||
/// Update the wrap_continuation flag.
|
||||
pub fn set_wrap_continuation(&mut self, v: bool) {
|
||||
self.0 = (self.0 & !(1 << 33)) | (u64::from(v) << 33);
|
||||
}
|
||||
/// Read the grapheme flag.
|
||||
pub fn grapheme(self) -> bool {
|
||||
self.0 & (1 << 34) != 0
|
||||
}
|
||||
/// Update the grapheme flag.
|
||||
pub fn set_grapheme(&mut self, v: bool) {
|
||||
self.0 = (self.0 & !(1 << 34)) | (u64::from(v) << 34);
|
||||
}
|
||||
/// Read the styled flag.
|
||||
pub fn styled(self) -> bool {
|
||||
self.0 & (1 << 35) != 0
|
||||
}
|
||||
/// Update the styled flag.
|
||||
pub fn set_styled(&mut self, v: bool) {
|
||||
self.0 = (self.0 & !(1 << 35)) | (u64::from(v) << 35);
|
||||
}
|
||||
/// Read the hyperlink flag.
|
||||
pub fn hyperlink(self) -> bool {
|
||||
self.0 & (1 << 36) != 0
|
||||
}
|
||||
/// Update the hyperlink flag.
|
||||
pub fn set_hyperlink(&mut self, v: bool) {
|
||||
self.0 = (self.0 & !(1 << 36)) | (u64::from(v) << 36);
|
||||
}
|
||||
/// Read the kitty_virtual_placeholder flag.
|
||||
pub fn kitty_virtual_placeholder(self) -> bool {
|
||||
self.0 & (1 << 39) != 0
|
||||
}
|
||||
/// Update the kitty_virtual_placeholder flag.
|
||||
pub fn set_kitty_virtual_placeholder(&mut self, v: bool) {
|
||||
self.0 = (self.0 & !(1 << 39)) | (u64::from(v) << 39);
|
||||
}
|
||||
/// Read the dirty flag.
|
||||
pub fn dirty(self) -> bool {
|
||||
self.0 & (1 << 40) != 0
|
||||
}
|
||||
/// Update the dirty flag.
|
||||
pub fn set_dirty(&mut self, v: bool) {
|
||||
self.0 = (self.0 & !(1 << 40)) | (u64::from(v) << 40);
|
||||
}
|
||||
}
|
||||
/// Current logical page dimensions.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Size {
|
||||
/// Columns currently addressable.
|
||||
pub cols: u16,
|
||||
/// Rows currently addressable.
|
||||
pub rows: u16,
|
||||
}
|
||||
/// Fixed logical allocation limits for a page.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Capacity {
|
||||
/// Allocated columns.
|
||||
pub cols: u16,
|
||||
/// Allocated rows.
|
||||
pub rows: u16,
|
||||
/// Requested style hash-table slots.
|
||||
pub styles: u16,
|
||||
/// Requested hyperlink-set bytes, excluding strings and map.
|
||||
pub hyperlink_bytes: u16,
|
||||
/// Requested grapheme bytes.
|
||||
pub grapheme_bytes: u32,
|
||||
/// Requested string bytes.
|
||||
pub string_bytes: u32,
|
||||
}
|
||||
impl Default for Capacity {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
cols: 0,
|
||||
rows: 0,
|
||||
styles: 16,
|
||||
hyperlink_bytes: 192,
|
||||
grapheme_bytes: GRAPHEME_BYTES_DEFAULT,
|
||||
string_bytes: STRING_BYTES_DEFAULT,
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Capacity adjustment that retains the original total byte allocation.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct Adjustment {
|
||||
/// New column count; rows absorb the size change.
|
||||
pub cols: Option<u16>,
|
||||
}
|
||||
impl Capacity {
|
||||
fn available_bytes_for_grid(self) -> usize {
|
||||
let l = Page::layout(self);
|
||||
let mut end = l.total_size;
|
||||
for (bytes, align) in [
|
||||
(l.hyperlink_map_layout.total_size, 4),
|
||||
(l.hyperlink_set_layout.total_size, 8),
|
||||
(l.string_alloc_layout.total_size, 8),
|
||||
(l.grapheme_map_layout.total_size, 8),
|
||||
(l.grapheme_alloc_layout.total_size, 8),
|
||||
(l.styles_layout.total_size, 8),
|
||||
] {
|
||||
end = (end - bytes) & !(align - 1);
|
||||
}
|
||||
end
|
||||
}
|
||||
/// Maximum columns that still allow one row without enlarging the page.
|
||||
pub fn max_cols(self) -> Option<u16> {
|
||||
let bytes = self.available_bytes_for_grid();
|
||||
(bytes > 8).then(|| ((bytes - 8) / 8).min(65535) as u16)
|
||||
}
|
||||
/// Adjust columns, retaining the same page size by changing row capacity.
|
||||
pub fn adjust(self, req: Adjustment) -> Result<Self, Error> {
|
||||
let Some(cols) = req.cols else {
|
||||
return Ok(self);
|
||||
};
|
||||
let rows = self.available_bytes_for_grid() / (8 + usize::from(cols) * 8);
|
||||
if rows == 0 {
|
||||
return Err(Error::OutOfMemory);
|
||||
}
|
||||
Ok(Self {
|
||||
cols,
|
||||
rows: rows.min(65535) as u16,
|
||||
..self
|
||||
})
|
||||
}
|
||||
}
|
||||
/// Logical bitmap allocator layout, preserving the reference size formula.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct AllocLayout {
|
||||
/// Total bytes including the reference allocator's chunk multiplier.
|
||||
pub total_size: usize,
|
||||
/// Number of 64-bit chunk bitmaps.
|
||||
pub bitmap_count: usize,
|
||||
/// Start of allocation data.
|
||||
pub chunks_start: usize,
|
||||
}
|
||||
impl AllocLayout {
|
||||
fn new(cap: usize, chunk: usize) -> Self {
|
||||
let aligned = cap.next_multiple_of(chunk);
|
||||
let bitmap_count = (aligned / chunk).div_ceil(64);
|
||||
Self {
|
||||
total_size: bitmap_count * 8 + aligned * chunk,
|
||||
bitmap_count,
|
||||
chunks_start: bitmap_count * 8,
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Logical offset hash-map layout.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct MapLayout {
|
||||
/// Total bytes including the 16-byte header.
|
||||
pub total_size: usize,
|
||||
/// Maximum entries (no extra load-factor reduction).
|
||||
pub capacity: usize,
|
||||
}
|
||||
impl MapLayout {
|
||||
fn new(capacity: usize, value_size: usize, value_align: usize) -> Self {
|
||||
let keys_start = (16 + capacity).next_multiple_of(4);
|
||||
let vals_start = (keys_start + capacity * 4).next_multiple_of(value_align);
|
||||
Self {
|
||||
total_size: (vals_start + capacity * value_size).next_multiple_of(value_align.max(4)),
|
||||
capacity,
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Complete logical page layout for the 64-bit Linux reference ABI.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Layout {
|
||||
/// Page-aligned byte size.
|
||||
pub total_size: usize,
|
||||
/// Row array offset.
|
||||
pub rows_start: usize,
|
||||
/// Row array bytes.
|
||||
pub rows_size: usize,
|
||||
/// Cell array offset.
|
||||
pub cells_start: usize,
|
||||
/// Cell array bytes.
|
||||
pub cells_size: usize,
|
||||
/// Style set offset.
|
||||
pub styles_start: usize,
|
||||
/// Style set layout.
|
||||
pub styles_layout: SetLayout,
|
||||
/// Grapheme allocator offset.
|
||||
pub grapheme_alloc_start: usize,
|
||||
/// Grapheme allocator layout.
|
||||
pub grapheme_alloc_layout: AllocLayout,
|
||||
/// Grapheme map offset.
|
||||
pub grapheme_map_start: usize,
|
||||
/// Grapheme map layout.
|
||||
pub grapheme_map_layout: MapLayout,
|
||||
/// String allocator offset.
|
||||
pub string_alloc_start: usize,
|
||||
/// String allocator layout.
|
||||
pub string_alloc_layout: AllocLayout,
|
||||
/// Hyperlink set offset.
|
||||
pub hyperlink_set_start: usize,
|
||||
/// Hyperlink set layout.
|
||||
pub hyperlink_set_layout: SetLayout,
|
||||
/// Hyperlink map offset.
|
||||
pub hyperlink_map_start: usize,
|
||||
/// Hyperlink map layout.
|
||||
pub hyperlink_map_layout: MapLayout,
|
||||
/// Requested capacity.
|
||||
pub capacity: Capacity,
|
||||
}
|
||||
/// Recoverable page operation errors.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
/// Allocation or grid capacity exhausted.
|
||||
OutOfMemory,
|
||||
/// Cell or row is outside the logical page.
|
||||
InvalidCoordinate,
|
||||
/// A grid dimension is zero.
|
||||
InvalidDimensions,
|
||||
/// Style set needs more space.
|
||||
StyleSetOutOfMemory,
|
||||
/// Style IDs need compaction.
|
||||
StyleSetNeedsRehash,
|
||||
/// No contiguous grapheme allocation fits.
|
||||
GraphemeAllocOutOfMemory,
|
||||
/// Grapheme map capacity exhausted.
|
||||
GraphemeMapOutOfMemory,
|
||||
/// Hyperlink map capacity exhausted.
|
||||
HyperlinkMapOutOfMemory,
|
||||
/// Hyperlink set capacity exhausted.
|
||||
SetOutOfMemory,
|
||||
/// Hyperlink IDs need compaction.
|
||||
SetNeedsRehash,
|
||||
/// No contiguous string allocation fits.
|
||||
StringsOutOfMemory,
|
||||
/// Missing style or hyperlink ID.
|
||||
InvalidId,
|
||||
/// Reference count overflow or underflow.
|
||||
InvalidRefCount,
|
||||
/// Capacity cannot grow further.
|
||||
OutOfSpace,
|
||||
}
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{self:?}")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for Error {}
|
||||
impl From<AddError> for Error {
|
||||
fn from(e: AddError) -> Self {
|
||||
match e {
|
||||
AddError::OutOfMemory => Self::StyleSetOutOfMemory,
|
||||
AddError::NeedsRehash => Self::StyleSetNeedsRehash,
|
||||
AddError::InvalidId => Self::InvalidId,
|
||||
AddError::InvalidRefCount => Self::InvalidRefCount,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Allocation {
|
||||
start: usize,
|
||||
len: usize,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
struct ChunkAlloc {
|
||||
used: Vec<bool>,
|
||||
chunk: usize,
|
||||
}
|
||||
impl ChunkAlloc {
|
||||
fn new(layout: AllocLayout, chunk: usize) -> Result<Self, Error> {
|
||||
let mut used = Vec::new();
|
||||
let count = layout.bitmap_count * 64;
|
||||
used.try_reserve_exact(count)
|
||||
.map_err(|_| Error::OutOfMemory)?;
|
||||
used.resize(count, false);
|
||||
Ok(Self { used, chunk })
|
||||
}
|
||||
fn alloc(&mut self, bytes: usize) -> Option<Allocation> {
|
||||
let n = bytes.div_ceil(self.chunk);
|
||||
if n == 0 {
|
||||
return Some(Allocation { start: 0, len: 0 });
|
||||
}
|
||||
let mut run = 0;
|
||||
for i in 0..self.used.len() {
|
||||
if self.used[i] {
|
||||
run = 0;
|
||||
} else {
|
||||
run += 1;
|
||||
}
|
||||
if run == n {
|
||||
let start = i + 1 - n;
|
||||
self.used[start..=i].fill(true);
|
||||
return Some(Allocation { start, len: n });
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
fn free(&mut self, a: Allocation) {
|
||||
self.used[a.start..a.start + a.len].fill(false);
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.used.fill(false);
|
||||
}
|
||||
}
|
||||
/// Page-owned rows, cells, and bounded auxiliary storage.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Page {
|
||||
/// Logical dimensions; no greater than capacity.
|
||||
pub size: Size,
|
||||
/// Fixed logical allocation limits.
|
||||
pub capacity: Capacity,
|
||||
/// A full-page operation dirtied every row.
|
||||
pub dirty: bool,
|
||||
/// Row metadata in screen order.
|
||||
pub rows: Vec<Row>,
|
||||
/// Cells in row-major capacity order.
|
||||
pub cells: Vec<Cell>,
|
||||
/// Shared nondefault cell styles.
|
||||
pub styles: style::Set,
|
||||
/// Shared hyperlink values.
|
||||
pub hyperlink_set: hyperlink::Set,
|
||||
grapheme_map: HashMap<usize, Vec<u32>>,
|
||||
grapheme_allocations: HashMap<usize, Allocation>,
|
||||
grapheme_alloc: ChunkAlloc,
|
||||
hyperlink_map: hyperlink::Map,
|
||||
string_alloc: ChunkAlloc,
|
||||
link_allocations: HashMap<u16, Vec<Allocation>>,
|
||||
pause_integrity_checks: usize,
|
||||
}
|
||||
/// A cell with its page-independent managed values, used when copying pages.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct OwnedCell {
|
||||
pub cell: Cell,
|
||||
pub style: Option<Style>,
|
||||
pub grapheme: Option<Vec<u32>>,
|
||||
pub hyperlink: Option<Hyperlink>,
|
||||
}
|
||||
impl Page {
|
||||
/// Compute the reference Zig layout without allocating memory.
|
||||
pub fn layout(cap: Capacity) -> Layout {
|
||||
let rows_start = 0;
|
||||
let rows_size = usize::from(cap.rows) * 8;
|
||||
let cells_start = rows_size;
|
||||
let cells_size = usize::from(cap.rows) * usize::from(cap.cols) * 8;
|
||||
let styles_start = (cells_start + cells_size).next_multiple_of(8);
|
||||
let styles_layout = SetLayout::init(usize::from(cap.styles), STYLE_ITEM_SIZE, 4);
|
||||
let grapheme_alloc_start = (styles_start + styles_layout.total_size).next_multiple_of(8);
|
||||
let grapheme_alloc_layout = AllocLayout::new(cap.grapheme_bytes as usize, 16);
|
||||
let grapheme_map_start =
|
||||
(grapheme_alloc_start + grapheme_alloc_layout.total_size).next_multiple_of(8);
|
||||
let gc = if cap.grapheme_bytes == 0 {
|
||||
0
|
||||
} else {
|
||||
(cap.grapheme_bytes as usize)
|
||||
.div_ceil(16)
|
||||
.next_power_of_two()
|
||||
};
|
||||
let grapheme_map_layout = MapLayout::new(gc, 16, 8);
|
||||
let string_alloc_start =
|
||||
(grapheme_map_start + grapheme_map_layout.total_size).next_multiple_of(8);
|
||||
let string_alloc_layout = AllocLayout::new(cap.string_bytes as usize, 32);
|
||||
let hyperlink_set_start =
|
||||
(string_alloc_start + string_alloc_layout.total_size).next_multiple_of(8);
|
||||
let hc = usize::from(cap.hyperlink_bytes) / HYPERLINK_ITEM_SIZE;
|
||||
let hyperlink_set_layout = SetLayout::init(hc, HYPERLINK_ITEM_SIZE, 8);
|
||||
let hyperlink_map_start =
|
||||
(hyperlink_set_start + hyperlink_set_layout.total_size).next_multiple_of(4);
|
||||
let hyperlink_map_layout = MapLayout::new(
|
||||
if hc == 0 {
|
||||
0
|
||||
} else {
|
||||
(hc * 16).next_power_of_two()
|
||||
},
|
||||
2,
|
||||
2,
|
||||
);
|
||||
let total_size =
|
||||
(hyperlink_map_start + hyperlink_map_layout.total_size).next_multiple_of(4096);
|
||||
Layout {
|
||||
total_size,
|
||||
rows_start,
|
||||
rows_size,
|
||||
cells_start,
|
||||
cells_size,
|
||||
styles_start,
|
||||
styles_layout,
|
||||
grapheme_alloc_start,
|
||||
grapheme_alloc_layout,
|
||||
grapheme_map_start,
|
||||
grapheme_map_layout,
|
||||
string_alloc_start,
|
||||
string_alloc_layout,
|
||||
hyperlink_set_start,
|
||||
hyperlink_set_layout,
|
||||
hyperlink_map_start,
|
||||
hyperlink_map_layout,
|
||||
capacity: cap,
|
||||
}
|
||||
}
|
||||
/// Allocate zeroed rows and cells. Initial logical size is the full capacity.
|
||||
pub fn init(cap: Capacity) -> Result<Self, Error> {
|
||||
if cap.cols == 0 || cap.rows == 0 {
|
||||
return Err(Error::InvalidDimensions);
|
||||
}
|
||||
let l = Self::layout(cap);
|
||||
if l.total_size > u32::MAX as usize {
|
||||
return Err(Error::OutOfSpace);
|
||||
}
|
||||
let mut rows = Vec::new();
|
||||
rows.try_reserve_exact(usize::from(cap.rows))
|
||||
.map_err(|_| Error::OutOfMemory)?;
|
||||
for y in 0..cap.rows {
|
||||
let mut row = Row::default();
|
||||
row.set_cells(usize::from(y) * usize::from(cap.cols));
|
||||
rows.push(row);
|
||||
}
|
||||
let mut cells = Vec::new();
|
||||
let count = usize::from(cap.cols) * usize::from(cap.rows);
|
||||
cells
|
||||
.try_reserve_exact(count)
|
||||
.map_err(|_| Error::OutOfMemory)?;
|
||||
cells.resize(count, Cell::default());
|
||||
Ok(Self {
|
||||
size: Size {
|
||||
rows: cap.rows,
|
||||
cols: cap.cols,
|
||||
},
|
||||
capacity: cap,
|
||||
dirty: false,
|
||||
rows,
|
||||
cells,
|
||||
styles: style::Set::init(usize::from(cap.styles))?,
|
||||
hyperlink_set: hyperlink::Set::init(usize::from(cap.hyperlink_bytes) / 48)?,
|
||||
grapheme_map: HashMap::new(),
|
||||
grapheme_allocations: HashMap::new(),
|
||||
grapheme_alloc: ChunkAlloc::new(l.grapheme_alloc_layout, 16)?,
|
||||
hyperlink_map: HashMap::new(),
|
||||
string_alloc: ChunkAlloc::new(l.string_alloc_layout, 32)?,
|
||||
link_allocations: HashMap::new(),
|
||||
pause_integrity_checks: 0,
|
||||
})
|
||||
}
|
||||
/// Reset contents, retaining grid and allocator backing storage.
|
||||
pub fn reinit(&mut self) -> Result<(), Error> {
|
||||
self.cells.fill(Cell::default());
|
||||
for (y, r) in self.rows.iter_mut().enumerate() {
|
||||
*r = Row::default();
|
||||
r.set_cells(y * usize::from(self.capacity.cols));
|
||||
}
|
||||
self.size = Size {
|
||||
cols: self.capacity.cols,
|
||||
rows: self.capacity.rows,
|
||||
};
|
||||
self.dirty = false;
|
||||
self.grapheme_map.clear();
|
||||
self.grapheme_allocations.clear();
|
||||
self.grapheme_alloc.reset();
|
||||
self.hyperlink_map.clear();
|
||||
self.string_alloc.reset();
|
||||
self.link_allocations.clear();
|
||||
self.styles = style::Set::init(usize::from(self.capacity.styles))?;
|
||||
self.hyperlink_set = hyperlink::Set::init(usize::from(self.capacity.hyperlink_bytes) / 48)?;
|
||||
Ok(())
|
||||
}
|
||||
/// Return row metadata by logical row number.
|
||||
pub fn get_row(&self, y: usize) -> Option<&Row> {
|
||||
self.rows.get(y).filter(|_| y < usize::from(self.size.rows))
|
||||
}
|
||||
/// Return mutable row metadata by logical row number.
|
||||
pub fn get_row_mut(&mut self, y: usize) -> Option<&mut Row> {
|
||||
self.rows
|
||||
.get_mut(y)
|
||||
.filter(|_| y < usize::from(self.size.rows))
|
||||
}
|
||||
/// Return the visible cells of a logical row in O(1).
|
||||
pub fn get_cells(&self, y: usize) -> Option<&[Cell]> {
|
||||
let start = self.get_row(y)?.cells();
|
||||
self.cells.get(start..start + usize::from(self.size.cols))
|
||||
}
|
||||
/// Return mutable visible cells of a logical row in O(1).
|
||||
pub fn get_cells_mut(&mut self, y: usize) -> Option<&mut [Cell]> {
|
||||
let start = self.get_row(y)?.cells();
|
||||
self.cells
|
||||
.get_mut(start..start + usize::from(self.size.cols))
|
||||
}
|
||||
/// Return a cell index for a logical coordinate.
|
||||
pub fn cell_index(&self, x: usize, y: usize) -> Result<usize, Error> {
|
||||
if x >= usize::from(self.size.cols) {
|
||||
return Err(Error::InvalidCoordinate);
|
||||
}
|
||||
Ok(self.get_row(y).ok_or(Error::InvalidCoordinate)?.cells() + x)
|
||||
}
|
||||
/// Return both metadata and cell at a logical coordinate.
|
||||
pub fn get_row_and_cell(&self, x: usize, y: usize) -> Option<(&Row, &Cell)> {
|
||||
Some((self.get_row(y)?, &self.cells[self.cell_index(x, y).ok()?]))
|
||||
}
|
||||
/// Resolve style without a map lookup for the default style.
|
||||
pub fn style(&self, cell: Cell) -> Style {
|
||||
if cell.style_id() == 0 {
|
||||
Style::default()
|
||||
} else {
|
||||
self.styles
|
||||
.get(cell.style_id())
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
/// Assign and retain styling, releasing the cell's previous style.
|
||||
pub fn set_style(&mut self, x: usize, y: usize, value: Style) -> Result<(), Error> {
|
||||
let i = self.cell_index(x, y)?;
|
||||
let id = if value.is_default() {
|
||||
0
|
||||
} else {
|
||||
self.styles.add(value)?
|
||||
};
|
||||
let old = self.cells[i].style_id();
|
||||
if old != 0 {
|
||||
self.styles.release(old)?;
|
||||
}
|
||||
self.cells[i].set_style_id(id);
|
||||
if id != 0 {
|
||||
self.rows[y].set_styled(true);
|
||||
}
|
||||
self.rows[y].set_dirty(true);
|
||||
Ok(())
|
||||
}
|
||||
/// Return additional codepoints, excluding the first stored on the cell.
|
||||
pub fn lookup_grapheme(&self, index: usize) -> Option<&[u32]> {
|
||||
self.grapheme_map.get(&index).map(Vec::as_slice)
|
||||
}
|
||||
/// Number of cells with grapheme data.
|
||||
pub fn grapheme_count(&self) -> usize {
|
||||
self.grapheme_map.len()
|
||||
}
|
||||
/// Maximum number of grapheme map entries.
|
||||
pub fn grapheme_capacity(&self) -> usize {
|
||||
Self::layout(self.capacity).grapheme_map_layout.capacity
|
||||
}
|
||||
/// Attach additional codepoints to a textual cell.
|
||||
pub fn set_graphemes(&mut self, x: usize, y: usize, cps: &[u32]) -> Result<(), Error> {
|
||||
let i = self.cell_index(x, y)?;
|
||||
if self.cells[i].codepoint() == 0 || cps.is_empty() || self.cells[i].has_grapheme() {
|
||||
return Err(Error::InvalidCoordinate);
|
||||
}
|
||||
if self.grapheme_count() >= self.grapheme_capacity() {
|
||||
return Err(Error::GraphemeMapOutOfMemory);
|
||||
}
|
||||
self.grapheme_map
|
||||
.try_reserve(1)
|
||||
.map_err(|_| Error::OutOfMemory)?;
|
||||
self.grapheme_allocations
|
||||
.try_reserve(1)
|
||||
.map_err(|_| Error::OutOfMemory)?;
|
||||
let mut data = Vec::new();
|
||||
data.try_reserve_exact(cps.len())
|
||||
.map_err(|_| Error::OutOfMemory)?;
|
||||
data.extend_from_slice(cps);
|
||||
let a = self
|
||||
.grapheme_alloc
|
||||
.alloc(cps.len() * 4)
|
||||
.ok_or(Error::GraphemeAllocOutOfMemory)?;
|
||||
self.grapheme_map.insert(i, data);
|
||||
self.grapheme_allocations.insert(i, a);
|
||||
self.cells[i].set_content_tag(ContentTag::CodepointGrapheme);
|
||||
self.rows[y].set_grapheme(true);
|
||||
self.rows[y].set_dirty(true);
|
||||
Ok(())
|
||||
}
|
||||
/// Append one additional codepoint, retaining allocation chunk semantics.
|
||||
pub fn append_grapheme(&mut self, x: usize, y: usize, cp: u32) -> Result<(), Error> {
|
||||
let i = self.cell_index(x, y)?;
|
||||
if !self.cells[i].has_grapheme() {
|
||||
return self.set_graphemes(x, y, &[cp]);
|
||||
}
|
||||
let data = self
|
||||
.grapheme_map
|
||||
.get_mut(&i)
|
||||
.ok_or(Error::InvalidCoordinate)?;
|
||||
data.try_reserve(1).map_err(|_| Error::OutOfMemory)?;
|
||||
if data.len().is_multiple_of(4) {
|
||||
let a = self
|
||||
.grapheme_alloc
|
||||
.alloc((data.len() + 1) * 4)
|
||||
.ok_or(Error::GraphemeAllocOutOfMemory)?;
|
||||
if let Some(old) = self.grapheme_allocations.insert(i, a) {
|
||||
self.grapheme_alloc.free(old);
|
||||
}
|
||||
}
|
||||
data.push(cp);
|
||||
self.rows[y].set_dirty(true);
|
||||
Ok(())
|
||||
}
|
||||
/// Release a cell's additional codepoints.
|
||||
pub fn clear_grapheme(&mut self, index: usize) {
|
||||
if self.grapheme_map.remove(&index).is_some()
|
||||
&& let Some(a) = self.grapheme_allocations.remove(&index)
|
||||
{
|
||||
self.grapheme_alloc.free(a);
|
||||
}
|
||||
if let Some(c) = self.cells.get_mut(index)
|
||||
&& c.has_grapheme()
|
||||
{
|
||||
c.set_content_tag(ContentTag::Codepoint);
|
||||
}
|
||||
}
|
||||
/// Move grapheme ownership between cell indices.
|
||||
pub fn move_grapheme(&mut self, src: usize, dst: usize) -> Result<(), Error> {
|
||||
if src >= self.cells.len() || dst >= self.cells.len() {
|
||||
return Err(Error::InvalidCoordinate);
|
||||
}
|
||||
if src == dst {
|
||||
return Ok(());
|
||||
}
|
||||
self.clear_grapheme(dst);
|
||||
if let Some(data) = self.grapheme_map.remove(&src) {
|
||||
self.grapheme_map.insert(dst, data);
|
||||
if let Some(a) = self.grapheme_allocations.remove(&src) {
|
||||
self.grapheme_allocations.insert(dst, a);
|
||||
}
|
||||
self.cells[src].set_content_tag(ContentTag::Codepoint);
|
||||
self.cells[dst].set_content_tag(ContentTag::CodepointGrapheme);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Look up a cell's page-local hyperlink ID.
|
||||
pub fn lookup_hyperlink(&self, index: usize) -> Option<u16> {
|
||||
self.hyperlink_map.get(&index).copied()
|
||||
}
|
||||
/// Number of hyperlink cells.
|
||||
pub fn hyperlink_count(&self) -> usize {
|
||||
self.hyperlink_map.len()
|
||||
}
|
||||
/// Maximum hyperlink-map entries.
|
||||
pub fn hyperlink_capacity(&self) -> usize {
|
||||
Self::layout(self.capacity).hyperlink_map_layout.capacity
|
||||
}
|
||||
/// Allocate strings and retain a page-local hyperlink.
|
||||
pub fn insert_hyperlink(&mut self, link: Hyperlink) -> Result<u16, Error> {
|
||||
let mut allocations = Vec::with_capacity(2);
|
||||
let uri = self
|
||||
.string_alloc
|
||||
.alloc(link.uri.len())
|
||||
.ok_or(Error::StringsOutOfMemory)?;
|
||||
allocations.push(uri);
|
||||
if let HyperlinkId::Explicit(v) = &link.id {
|
||||
if let Some(a) = self.string_alloc.alloc(v.len()) {
|
||||
allocations.push(a);
|
||||
} else {
|
||||
self.string_alloc.free(uri);
|
||||
return Err(Error::StringsOutOfMemory);
|
||||
}
|
||||
}
|
||||
let result = self.hyperlink_set.add(link);
|
||||
let id = match result {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
for a in allocations {
|
||||
self.string_alloc.free(a);
|
||||
}
|
||||
return Err(match e {
|
||||
AddError::NeedsRehash => Error::SetNeedsRehash,
|
||||
_ => Error::SetOutOfMemory,
|
||||
});
|
||||
}
|
||||
};
|
||||
// Trimmed set entries no longer own their retained string chunks.
|
||||
let dead: Vec<_> = self
|
||||
.link_allocations
|
||||
.keys()
|
||||
.copied()
|
||||
.filter(|id| !self.hyperlink_set.contains_slot(*id))
|
||||
.collect();
|
||||
for id in dead {
|
||||
if let Some(v) = self.link_allocations.remove(&id) {
|
||||
for a in v {
|
||||
self.string_alloc.free(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.hyperlink_set.ref_count(id) > 1 {
|
||||
for a in allocations {
|
||||
self.string_alloc.free(a);
|
||||
}
|
||||
} else if let Some(v) = self.link_allocations.insert(id, allocations) {
|
||||
for a in v {
|
||||
self.string_alloc.free(a);
|
||||
}
|
||||
}
|
||||
Ok(id)
|
||||
}
|
||||
/// Set a hyperlink, consuming an existing retained reference (no increment).
|
||||
pub fn set_hyperlink(&mut self, x: usize, y: usize, id: u16) -> Result<(), Error> {
|
||||
let i = self.cell_index(x, y)?;
|
||||
if self.hyperlink_set.get(id).is_none() {
|
||||
return Err(Error::InvalidId);
|
||||
}
|
||||
if !self.hyperlink_map.contains_key(&i)
|
||||
&& self.hyperlink_count() >= self.hyperlink_capacity()
|
||||
{
|
||||
return Err(Error::HyperlinkMapOutOfMemory);
|
||||
}
|
||||
self.hyperlink_map
|
||||
.try_reserve(1)
|
||||
.map_err(|_| Error::OutOfMemory)?;
|
||||
self.clear_hyperlink(i)?;
|
||||
self.hyperlink_map.insert(i, id);
|
||||
self.cells[i].set_hyperlink(true);
|
||||
self.rows[y].set_hyperlink(true);
|
||||
Ok(())
|
||||
}
|
||||
/// Release a cell's hyperlink reference.
|
||||
pub fn clear_hyperlink(&mut self, i: usize) -> Result<(), Error> {
|
||||
if let Some(id) = self.hyperlink_map.remove(&i) {
|
||||
self.hyperlink_set.release(id)?;
|
||||
}
|
||||
if let Some(c) = self.cells.get_mut(i) {
|
||||
c.set_hyperlink(false);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Recompute the conservative row grapheme flag.
|
||||
pub fn update_row_grapheme_flag(&mut self, y: usize) {
|
||||
if let Some(c) = self.get_cells(y) {
|
||||
let v = c.iter().any(|c| c.has_grapheme());
|
||||
self.rows[y].set_grapheme(v);
|
||||
}
|
||||
}
|
||||
/// Recompute the conservative row style flag.
|
||||
pub fn update_row_styled_flag(&mut self, y: usize) {
|
||||
if let Some(c) = self.get_cells(y) {
|
||||
let v = c.iter().any(|c| c.has_styling());
|
||||
self.rows[y].set_styled(v);
|
||||
}
|
||||
}
|
||||
/// Recompute the conservative row hyperlink flag.
|
||||
pub fn update_row_hyperlink_flag(&mut self, y: usize) {
|
||||
if let Some(c) = self.get_cells(y) {
|
||||
let v = c.iter().any(|c| c.hyperlink());
|
||||
self.rows[y].set_hyperlink(v);
|
||||
}
|
||||
}
|
||||
/// True if the page or any visible row is dirty.
|
||||
pub fn is_dirty(&self) -> bool {
|
||||
self.dirty
|
||||
|| self.rows[..usize::from(self.size.rows)]
|
||||
.iter()
|
||||
.any(|r| r.dirty())
|
||||
}
|
||||
/// Clear a half-open column range, optionally preserving protected cells.
|
||||
pub fn clear_cells(
|
||||
&mut self,
|
||||
y: usize,
|
||||
start: usize,
|
||||
end: usize,
|
||||
protect: bool,
|
||||
) -> Result<(), Error> {
|
||||
if start > end || end > usize::from(self.size.cols) || y >= usize::from(self.size.rows) {
|
||||
return Err(Error::InvalidCoordinate);
|
||||
}
|
||||
let base = self.rows[y].cells();
|
||||
for i in base + start..base + end {
|
||||
let c = self.cells[i];
|
||||
if protect && c.protected() {
|
||||
continue;
|
||||
}
|
||||
if c.has_styling() {
|
||||
self.styles.release(c.style_id())?;
|
||||
}
|
||||
if c.has_grapheme() {
|
||||
self.clear_grapheme(i);
|
||||
}
|
||||
if c.hyperlink() {
|
||||
self.clear_hyperlink(i)?;
|
||||
}
|
||||
self.cells[i] = Cell::default();
|
||||
}
|
||||
self.rows[y].set_dirty(true);
|
||||
Ok(())
|
||||
}
|
||||
/// Move a half-open cell range, clearing overwritten values and the source.
|
||||
pub fn move_cells(
|
||||
&mut self,
|
||||
src_y: usize,
|
||||
src_x: usize,
|
||||
dst_y: usize,
|
||||
dst_x: usize,
|
||||
len: usize,
|
||||
) -> Result<(), Error> {
|
||||
if src_x + len > usize::from(self.size.cols) || dst_x + len > usize::from(self.size.cols) {
|
||||
return Err(Error::InvalidCoordinate);
|
||||
}
|
||||
let src = self.cell_index(src_x, src_y)?;
|
||||
let dst = self.cell_index(dst_x, dst_y)?;
|
||||
if src == dst {
|
||||
return Ok(());
|
||||
}
|
||||
// Move in memmove order; auxiliary maps follow the cell indices.
|
||||
for n in 0..len {
|
||||
let n = if dst > src { len - 1 - n } else { n };
|
||||
let si = src + n;
|
||||
let di = dst + n;
|
||||
self.clear_cells(dst_y, dst_x + n, dst_x + n + 1, false)?;
|
||||
let c = self.cells[si];
|
||||
self.cells[di] = c;
|
||||
self.cells[si] = Cell::default();
|
||||
if c.has_grapheme() {
|
||||
if let Some(v) = self.grapheme_map.remove(&si) {
|
||||
self.grapheme_map.insert(di, v);
|
||||
}
|
||||
if let Some(a) = self.grapheme_allocations.remove(&si) {
|
||||
self.grapheme_allocations.insert(di, a);
|
||||
}
|
||||
}
|
||||
if c.hyperlink()
|
||||
&& let Some(v) = self.hyperlink_map.remove(&si)
|
||||
{
|
||||
self.hyperlink_map.insert(di, v);
|
||||
}
|
||||
}
|
||||
for y in [src_y, dst_y] {
|
||||
self.update_row_grapheme_flag(y);
|
||||
self.update_row_styled_flag(y);
|
||||
self.update_row_hyperlink_flag(y);
|
||||
self.rows[y].set_dirty(true);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Swap two cells and their managed values without changing reference counts.
|
||||
pub fn swap_cells(&mut self, ax: usize, ay: usize, bx: usize, by: usize) -> Result<(), Error> {
|
||||
let a = self.cell_index(ax, ay)?;
|
||||
let b = self.cell_index(bx, by)?;
|
||||
if a == b {
|
||||
return Ok(());
|
||||
}
|
||||
self.cells.swap(a, b);
|
||||
|
||||
swap_map(&mut self.grapheme_map, a, b);
|
||||
swap_map(&mut self.grapheme_allocations, a, b);
|
||||
swap_map(&mut self.hyperlink_map, a, b);
|
||||
for y in [ay, by] {
|
||||
self.update_row_grapheme_flag(y);
|
||||
self.update_row_styled_flag(y);
|
||||
self.update_row_hyperlink_flag(y);
|
||||
self.rows[y].set_dirty(true);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub(crate) fn export_cell(&self, i: usize) -> OwnedCell {
|
||||
let cell = self.cells[i];
|
||||
OwnedCell {
|
||||
cell,
|
||||
style: if cell.has_styling() {
|
||||
self.styles.get(cell.style_id()).copied()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
grapheme: if cell.has_grapheme() {
|
||||
self.grapheme_map.get(&i).cloned()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
hyperlink: if cell.hyperlink() {
|
||||
self.lookup_hyperlink(i)
|
||||
.and_then(|id| self.hyperlink_set.get(id))
|
||||
.cloned()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
pub(crate) fn import_cell(&mut self, x: usize, y: usize, v: &OwnedCell) -> Result<(), Error> {
|
||||
let i = self.cell_index(x, y)?;
|
||||
self.clear_cells(y, x, x + 1, false)?;
|
||||
let mut c = v.cell;
|
||||
c.set_style_id(0);
|
||||
c.set_hyperlink(false);
|
||||
if c.has_grapheme() {
|
||||
c.set_content_tag(ContentTag::Codepoint);
|
||||
}
|
||||
self.cells[i] = c;
|
||||
if let Some(s) = v.style {
|
||||
self.set_style(x, y, s)?;
|
||||
}
|
||||
if let Some(g) = &v.grapheme {
|
||||
self.set_graphemes(x, y, g)?;
|
||||
}
|
||||
if let Some(h) = &v.hyperlink {
|
||||
let id = if let Some(id) = self.hyperlink_set.lookup(h) {
|
||||
self.hyperlink_set.r#use(id)?;
|
||||
id
|
||||
} else {
|
||||
self.insert_hyperlink(h.clone())?
|
||||
};
|
||||
if let Err(e) = self.set_hyperlink(x, y, id) {
|
||||
self.hyperlink_set.release(id)?;
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Clone rows from another page, truncating or padding columns as needed.
|
||||
pub fn clone_from(&mut self, src: &Self, y_start: usize, y_end: usize) -> Result<(), Error> {
|
||||
if y_start > y_end
|
||||
|| y_end > usize::from(src.size.rows)
|
||||
|| y_end - y_start > usize::from(self.capacity.rows)
|
||||
{
|
||||
return Err(Error::InvalidCoordinate);
|
||||
}
|
||||
for y in 0..y_end - y_start {
|
||||
self.clone_row_from(src, y_start + y, y)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Clone one complete row to a destination row.
|
||||
pub fn clone_row_from(&mut self, src: &Self, src_y: usize, dst_y: usize) -> Result<(), Error> {
|
||||
let cols = usize::from(self.size.cols.min(src.size.cols));
|
||||
self.clear_cells(dst_y, 0, usize::from(self.size.cols), false)?;
|
||||
self.clone_partial_row_from(src, src_y, dst_y, 0, 0, cols)?;
|
||||
let start = self.rows[dst_y].cells();
|
||||
self.rows[dst_y] = *src.get_row(src_y).ok_or(Error::InvalidCoordinate)?;
|
||||
self.rows[dst_y].set_cells(start);
|
||||
if self.size.cols > src.size.cols {
|
||||
let i = self.cell_index(usize::from(src.size.cols - 1), dst_y)?;
|
||||
if self.cells[i].wide() == Wide::SpacerHead {
|
||||
self.cells[i].set_wide(Wide::Narrow);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Clone a column range, preserving untouched destination cells.
|
||||
pub fn clone_partial_row_from(
|
||||
&mut self,
|
||||
src: &Self,
|
||||
src_y: usize,
|
||||
dst_y: usize,
|
||||
src_x: usize,
|
||||
dst_x: usize,
|
||||
len: usize,
|
||||
) -> Result<(), Error> {
|
||||
if src_x
|
||||
.checked_add(len)
|
||||
.is_none_or(|end| end > usize::from(src.size.cols))
|
||||
|| dst_x
|
||||
.checked_add(len)
|
||||
.is_none_or(|end| end > usize::from(self.size.cols))
|
||||
{
|
||||
return Err(Error::InvalidCoordinate);
|
||||
}
|
||||
let source_row = *src.get_row(src_y).ok_or(Error::InvalidCoordinate)?;
|
||||
let original_row = *self.get_row(dst_y).ok_or(Error::InvalidCoordinate)?;
|
||||
self.clear_cells(dst_y, dst_x, dst_x + len, false)?;
|
||||
if source_row.managed_memory() {
|
||||
for n in 0..len {
|
||||
let i = src.cell_index(src_x + n, src_y)?;
|
||||
self.import_cell(dst_x + n, dst_y, &src.export_cell(i))?;
|
||||
}
|
||||
} else {
|
||||
let from = source_row.cells() + src_x;
|
||||
let to = original_row.cells() + dst_x;
|
||||
self.cells[to..to + len].copy_from_slice(&src.cells[from..from + len]);
|
||||
}
|
||||
let managed = self.rows[dst_y];
|
||||
let mut metadata = source_row;
|
||||
metadata.set_cells(original_row.cells());
|
||||
if len < usize::from(self.size.cols) {
|
||||
metadata.set_wrap(original_row.wrap());
|
||||
metadata.set_wrap_continuation(original_row.wrap_continuation());
|
||||
metadata.set_grapheme(managed.grapheme());
|
||||
metadata.set_styled(managed.styled());
|
||||
metadata.set_hyperlink(managed.hyperlink());
|
||||
metadata.set_dirty(source_row.dirty() || original_row.dirty());
|
||||
}
|
||||
self.rows[dst_y] = metadata;
|
||||
Ok(())
|
||||
}
|
||||
/// Clone a range within the same page, including overlapping ranges.
|
||||
pub fn clone_cells_within(
|
||||
&mut self,
|
||||
src_y: usize,
|
||||
dst_y: usize,
|
||||
src_x: usize,
|
||||
dst_x: usize,
|
||||
len: usize,
|
||||
) -> Result<(), Error> {
|
||||
if src_x + len > usize::from(self.size.cols) || dst_x + len > usize::from(self.size.cols) {
|
||||
return Err(Error::InvalidCoordinate);
|
||||
}
|
||||
let src = self.cell_index(src_x, src_y)?;
|
||||
self.cell_index(dst_x, dst_y)?;
|
||||
let data: Vec<_> = (src..src + len).map(|i| self.export_cell(i)).collect();
|
||||
for (n, v) in data.iter().enumerate() {
|
||||
self.import_cell(dst_x + n, dst_y, v)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Minimum capacity that can clone the half-open row range.
|
||||
pub fn exact_row_capacity(&self, start: usize, end: usize) -> Result<Capacity, Error> {
|
||||
if start >= end || end > usize::from(self.size.rows) {
|
||||
return Err(Error::InvalidCoordinate);
|
||||
}
|
||||
let mut styles = HashSet::new();
|
||||
let mut links = HashSet::new();
|
||||
let mut gb = 0;
|
||||
let mut sb = 0;
|
||||
let mut hc: usize = 0;
|
||||
for y in start..end {
|
||||
let base = self.rows[y].cells();
|
||||
for x in 0..usize::from(self.size.cols) {
|
||||
let i = base + x;
|
||||
let c = self.cells[i];
|
||||
if c.has_styling() {
|
||||
styles.insert(c.style_id());
|
||||
}
|
||||
if let Some(g) = self.lookup_grapheme(i) {
|
||||
gb += (g.len() * 4).next_multiple_of(16);
|
||||
}
|
||||
if let Some(id) = self.lookup_hyperlink(i) {
|
||||
hc += 1;
|
||||
if links.insert(id)
|
||||
&& let Some(h) = self.hyperlink_set.get(id)
|
||||
{
|
||||
sb += h.string_bytes();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let sc = style::Set::capacity_for_count(styles.len());
|
||||
let lc = hyperlink::Set::capacity_for_count(links.len()).max(hc.div_ceil(16));
|
||||
Ok(Capacity {
|
||||
cols: self.size.cols,
|
||||
rows: (end - start) as u16,
|
||||
styles: u16::try_from(sc).map_err(|_| Error::OutOfSpace)?,
|
||||
hyperlink_bytes: u16::try_from(lc * 48).map_err(|_| Error::OutOfSpace)?,
|
||||
grapheme_bytes: u32::try_from(gb).map_err(|_| Error::OutOfSpace)?,
|
||||
string_bytes: u32::try_from(sb).map_err(|_| Error::OutOfSpace)?,
|
||||
})
|
||||
}
|
||||
/// Nestable suspension of debug integrity checks.
|
||||
pub fn pause_integrity_checks(&mut self, pause: bool) {
|
||||
if pause {
|
||||
self.pause_integrity_checks += 1;
|
||||
} else {
|
||||
self.pause_integrity_checks = self.pause_integrity_checks.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Structural invariant failures reported by debug/test verification.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum IntegrityError {
|
||||
/// ZeroRowCount invariant violation.
|
||||
ZeroRowCount,
|
||||
/// ZeroColCount invariant violation.
|
||||
ZeroColCount,
|
||||
/// UnmarkedGraphemeRow invariant violation.
|
||||
UnmarkedGraphemeRow,
|
||||
/// MissingGraphemeData invariant violation.
|
||||
MissingGraphemeData,
|
||||
/// InvalidGraphemeCount invariant violation.
|
||||
InvalidGraphemeCount,
|
||||
/// UnmarkedGraphemeCell invariant violation.
|
||||
UnmarkedGraphemeCell,
|
||||
/// MissingStyle invariant violation.
|
||||
MissingStyle,
|
||||
/// UnmarkedStyleRow invariant violation.
|
||||
UnmarkedStyleRow,
|
||||
/// MismatchedStyleRef invariant violation.
|
||||
MismatchedStyleRef,
|
||||
/// InvalidStyleCount invariant violation.
|
||||
InvalidStyleCount,
|
||||
/// MissingHyperlinkData invariant violation.
|
||||
MissingHyperlinkData,
|
||||
/// MismatchedHyperlinkRef invariant violation.
|
||||
MismatchedHyperlinkRef,
|
||||
/// UnmarkedHyperlinkCell invariant violation.
|
||||
UnmarkedHyperlinkCell,
|
||||
/// UnmarkedHyperlinkRow invariant violation.
|
||||
UnmarkedHyperlinkRow,
|
||||
/// InvalidSpacerTailLocation invariant violation.
|
||||
InvalidSpacerTailLocation,
|
||||
/// InvalidSpacerHeadLocation invariant violation.
|
||||
InvalidSpacerHeadLocation,
|
||||
/// UnwrappedSpacerHead invariant violation.
|
||||
UnwrappedSpacerHead,
|
||||
/// InvalidDimensions invariant violation.
|
||||
InvalidDimensions,
|
||||
}
|
||||
#[cfg(any(debug_assertions, test))]
|
||||
impl Page {
|
||||
/// Verify grid, metadata, auxiliary maps, and reference counts.
|
||||
pub fn verify_integrity(&self) -> Result<(), IntegrityError> {
|
||||
use IntegrityError as E;
|
||||
if self.pause_integrity_checks > 0 {
|
||||
return Ok(());
|
||||
}
|
||||
if self.size.rows == 0 {
|
||||
return Err(E::ZeroRowCount);
|
||||
}
|
||||
if self.size.cols == 0 {
|
||||
return Err(E::ZeroColCount);
|
||||
}
|
||||
if self.size.rows > self.capacity.rows || self.size.cols > self.capacity.cols {
|
||||
return Err(E::InvalidDimensions);
|
||||
}
|
||||
let mut styles = HashMap::<u16, usize>::new();
|
||||
let mut links = HashMap::<u16, usize>::new();
|
||||
let mut graphemes = 0;
|
||||
for y in 0..usize::from(self.size.rows) {
|
||||
let r = self.rows[y];
|
||||
let base = r.cells();
|
||||
for x in 0..usize::from(self.size.cols) {
|
||||
let i = base + x;
|
||||
let c = self.cells[i];
|
||||
if c.has_grapheme() {
|
||||
graphemes += 1;
|
||||
if !r.grapheme() {
|
||||
return Err(E::UnmarkedGraphemeRow);
|
||||
}
|
||||
let g = self.lookup_grapheme(i).ok_or(E::MissingGraphemeData)?;
|
||||
if g.is_empty() {
|
||||
return Err(E::InvalidGraphemeCount);
|
||||
}
|
||||
} else if self.grapheme_map.contains_key(&i) {
|
||||
return Err(E::UnmarkedGraphemeCell);
|
||||
}
|
||||
if c.has_styling() {
|
||||
if !r.styled() {
|
||||
return Err(E::UnmarkedStyleRow);
|
||||
}
|
||||
if self.styles.get(c.style_id()).is_none() {
|
||||
return Err(E::MissingStyle);
|
||||
}
|
||||
*styles.entry(c.style_id()).or_default() += 1;
|
||||
}
|
||||
if c.hyperlink() {
|
||||
if !r.hyperlink() {
|
||||
return Err(E::UnmarkedHyperlinkRow);
|
||||
}
|
||||
let id = self.lookup_hyperlink(i).ok_or(E::MissingHyperlinkData)?;
|
||||
if self.hyperlink_set.get(id).is_none() {
|
||||
return Err(E::MissingHyperlinkData);
|
||||
}
|
||||
*links.entry(id).or_default() += 1;
|
||||
} else if self.hyperlink_map.contains_key(&i) {
|
||||
return Err(E::UnmarkedHyperlinkCell);
|
||||
}
|
||||
match c.wide() {
|
||||
Wide::SpacerTail => {
|
||||
if x == 0 || self.cells[i - 1].wide() != Wide::Wide {
|
||||
return Err(E::InvalidSpacerTailLocation);
|
||||
}
|
||||
}
|
||||
Wide::SpacerHead => {
|
||||
if x + 1 != usize::from(self.size.cols) {
|
||||
return Err(E::InvalidSpacerHeadLocation);
|
||||
}
|
||||
if !r.wrap() {
|
||||
return Err(E::UnwrappedSpacerHead);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if graphemes > self.grapheme_count() {
|
||||
return Err(E::InvalidGraphemeCount);
|
||||
}
|
||||
for (id, _, refs) in self.styles.entries() {
|
||||
if styles.get(&id).copied().unwrap_or(0) > usize::from(refs) {
|
||||
return Err(E::MismatchedStyleRef);
|
||||
}
|
||||
}
|
||||
for (id, _, refs) in self.hyperlink_set.entries() {
|
||||
if links.get(&id).copied().unwrap_or(0) > usize::from(refs) {
|
||||
return Err(E::MismatchedHyperlinkRef);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Assert structural integrity in debug/test builds.
|
||||
pub fn assert_integrity(&self) {
|
||||
assert_eq!(self.verify_integrity(), Ok(()));
|
||||
}
|
||||
}
|
||||
|
||||
fn swap_map<T>(map: &mut HashMap<usize, T>, a: usize, b: usize) {
|
||||
let av = map.remove(&a);
|
||||
let bv = map.remove(&b);
|
||||
if let Some(v) = av {
|
||||
map.insert(b, v);
|
||||
}
|
||||
if let Some(v) = bv {
|
||||
map.insert(a, v);
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::style::{Color, Flags};
|
||||
fn page() -> Page {
|
||||
Page::init(Capacity {
|
||||
cols: 10,
|
||||
rows: 10,
|
||||
styles: 8,
|
||||
..Capacity::default()
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
fn write(p: &mut Page, x: usize, y: usize, cp: u32) {
|
||||
let i = p.cell_index(x, y).unwrap();
|
||||
p.cells[i] = Cell::init(cp);
|
||||
}
|
||||
fn cp(p: &Page, x: usize, y: usize) -> u32 {
|
||||
p.get_row_and_cell(x, y).unwrap().1.codepoint()
|
||||
}
|
||||
fn bold() -> Style {
|
||||
Style {
|
||||
flags: Flags {
|
||||
bold: true,
|
||||
..Flags::default()
|
||||
},
|
||||
..Style::default()
|
||||
}
|
||||
}
|
||||
fn link() -> Hyperlink {
|
||||
Hyperlink {
|
||||
id: HyperlinkId::Implicit(1),
|
||||
uri: b"https://example.com".to_vec(),
|
||||
}
|
||||
}
|
||||
fn clone_exact(p: &Page, end: usize) -> Page {
|
||||
let cap = p.exact_row_capacity(0, end).unwrap();
|
||||
let mut q = Page::init(cap).unwrap();
|
||||
q.clone_from(p, 0, end).unwrap();
|
||||
assert_eq!(q.exact_row_capacity(0, end).unwrap(), cap);
|
||||
q.assert_integrity();
|
||||
q
|
||||
}
|
||||
#[test]
|
||||
fn page_layout_can_take_a_maxed_capacity() {
|
||||
let l = Page::layout(Capacity {
|
||||
cols: u16::MAX,
|
||||
rows: u16::MAX,
|
||||
styles: u16::MAX,
|
||||
hyperlink_bytes: u16::MAX,
|
||||
grapheme_bytes: u32::MAX,
|
||||
string_bytes: u32::MAX,
|
||||
});
|
||||
assert!(l.total_size > u32::MAX as usize);
|
||||
}
|
||||
#[test]
|
||||
fn cell_is_zero_by_default() {
|
||||
let c = Cell::init(0);
|
||||
assert_eq!(c.raw(), 0);
|
||||
assert_eq!(c.semantic_content(), SemanticContent::Output);
|
||||
assert_eq!(std::mem::size_of::<Cell>(), 8);
|
||||
assert_eq!(std::mem::size_of::<Row>(), 8);
|
||||
}
|
||||
#[test]
|
||||
fn page_capacity_adjust_cols_down() {
|
||||
let original = Page::layout(STD_CAPACITY).total_size;
|
||||
let cap = STD_CAPACITY
|
||||
.adjust(Adjustment {
|
||||
cols: Some(STD_CAPACITY.cols / 2),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(Page::layout(cap).total_size, original);
|
||||
assert!(
|
||||
Page::layout(Capacity {
|
||||
rows: cap.rows + 1,
|
||||
..cap
|
||||
})
|
||||
.total_size
|
||||
> original
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn page_capacity_adjust_cols_down_to_1() {
|
||||
let original = Page::layout(STD_CAPACITY).total_size;
|
||||
let cap = STD_CAPACITY.adjust(Adjustment { cols: Some(1) }).unwrap();
|
||||
assert_eq!(Page::layout(cap).total_size, original);
|
||||
assert!(
|
||||
Page::layout(Capacity {
|
||||
rows: cap.rows + 1,
|
||||
..cap
|
||||
})
|
||||
.total_size
|
||||
> original
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn page_capacity_adjust_cols_up() {
|
||||
let original = Page::layout(STD_CAPACITY).total_size;
|
||||
let cap = STD_CAPACITY
|
||||
.adjust(Adjustment {
|
||||
cols: Some(STD_CAPACITY.cols * 2),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(Page::layout(cap).total_size, original);
|
||||
assert!(
|
||||
Page::layout(Capacity {
|
||||
rows: cap.rows + 1,
|
||||
..cap
|
||||
})
|
||||
.total_size
|
||||
> original
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn page_capacity_adjust_cols_sweep() {
|
||||
let original = Page::layout(STD_CAPACITY).total_size;
|
||||
let mut cap = STD_CAPACITY;
|
||||
for cols in 1..STD_CAPACITY.cols * 2 {
|
||||
cap = cap.adjust(Adjustment { cols: Some(cols) }).unwrap();
|
||||
assert_eq!(Page::layout(cap).total_size, original);
|
||||
assert!(
|
||||
Page::layout(Capacity {
|
||||
rows: cap.rows + 1,
|
||||
..cap
|
||||
})
|
||||
.total_size
|
||||
> original
|
||||
);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn page_capacity_adjust_cols_too_high() {
|
||||
assert_eq!(
|
||||
STD_CAPACITY.adjust(Adjustment {
|
||||
cols: Some(u16::MAX)
|
||||
}),
|
||||
Err(Error::OutOfMemory)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn capacity_max_cols_basic() {
|
||||
let max = STD_CAPACITY.max_cols().unwrap();
|
||||
assert!(max >= STD_CAPACITY.cols);
|
||||
assert!(
|
||||
STD_CAPACITY
|
||||
.adjust(Adjustment { cols: Some(max) })
|
||||
.unwrap()
|
||||
.rows
|
||||
>= 1
|
||||
);
|
||||
assert_eq!(
|
||||
STD_CAPACITY.adjust(Adjustment {
|
||||
cols: Some(max + 1)
|
||||
}),
|
||||
Err(Error::OutOfMemory)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn capacity_max_cols_preserves_total_size() {
|
||||
let cap = STD_CAPACITY
|
||||
.adjust(Adjustment {
|
||||
cols: STD_CAPACITY.max_cols(),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
Page::layout(cap).total_size,
|
||||
Page::layout(STD_CAPACITY).total_size
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn capacity_max_cols_with_1_row_exactly() {
|
||||
assert_eq!(
|
||||
STD_CAPACITY
|
||||
.adjust(Adjustment {
|
||||
cols: STD_CAPACITY.max_cols()
|
||||
})
|
||||
.unwrap()
|
||||
.rows,
|
||||
1
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn page_init() {
|
||||
let p = Page::init(Capacity {
|
||||
cols: 120,
|
||||
rows: 80,
|
||||
styles: 32,
|
||||
..Capacity::default()
|
||||
})
|
||||
.unwrap();
|
||||
p.assert_integrity();
|
||||
}
|
||||
#[test]
|
||||
fn page_read_and_write_cells() {
|
||||
let mut p = page();
|
||||
for y in 0..10 {
|
||||
write(&mut p, 1, y, y as u32);
|
||||
}
|
||||
for y in 0..10 {
|
||||
assert_eq!(cp(&p, 1, y), y as u32);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn page_append_grapheme_small() {
|
||||
let mut p = page();
|
||||
write(&mut p, 0, 0, 9);
|
||||
p.append_grapheme(0, 0, 10).unwrap();
|
||||
assert!(p.rows[0].grapheme());
|
||||
assert_eq!(p.lookup_grapheme(0), Some([10].as_slice()));
|
||||
p.append_grapheme(0, 0, 11).unwrap();
|
||||
assert_eq!(p.lookup_grapheme(0), Some([10, 11].as_slice()));
|
||||
p.clear_grapheme(0);
|
||||
p.update_row_grapheme_flag(0);
|
||||
assert!(!p.rows[0].grapheme());
|
||||
assert!(!p.cells[0].has_grapheme());
|
||||
}
|
||||
#[test]
|
||||
fn page_append_grapheme_larger_than_chunk() {
|
||||
let mut p = page();
|
||||
write(&mut p, 0, 0, 9);
|
||||
for i in 0..40 {
|
||||
p.append_grapheme(0, 0, 10 + i).unwrap();
|
||||
}
|
||||
assert_eq!(p.lookup_grapheme(0).unwrap(), (10..50).collect::<Vec<_>>());
|
||||
}
|
||||
#[test]
|
||||
fn page_clear_grapheme_not_all_cells() {
|
||||
let mut p = page();
|
||||
for x in 0..2 {
|
||||
write(&mut p, x, 0, 9);
|
||||
p.append_grapheme(x, 0, 10).unwrap();
|
||||
}
|
||||
p.clear_grapheme(0);
|
||||
p.update_row_grapheme_flag(0);
|
||||
assert!(p.rows[0].grapheme());
|
||||
assert!(!p.cells[0].has_grapheme());
|
||||
assert!(p.cells[1].has_grapheme());
|
||||
}
|
||||
#[test]
|
||||
fn page_clone() {
|
||||
let mut p = page();
|
||||
for y in 0..10 {
|
||||
write(&mut p, 1, y, y as u32);
|
||||
}
|
||||
let q = p.clone();
|
||||
assert_eq!(p.capacity, q.capacity);
|
||||
for y in 0..10 {
|
||||
write(&mut p, 1, y, 0);
|
||||
assert_eq!(cp(&q, 1, y), y as u32);
|
||||
assert_eq!(cp(&p, 1, y), 0);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn page_clone_from() {
|
||||
let mut p = page();
|
||||
for y in 0..10 {
|
||||
write(&mut p, 1, y, y as u32);
|
||||
}
|
||||
let q = {
|
||||
let mut q = page();
|
||||
q.clone_from(&p, 0, 10).unwrap();
|
||||
q
|
||||
};
|
||||
assert_eq!(p.capacity, q.capacity);
|
||||
for y in 0..10 {
|
||||
write(&mut p, 1, y, 0);
|
||||
assert_eq!(cp(&q, 1, y), y as u32);
|
||||
assert_eq!(cp(&p, 1, y), 0);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn page_clone_graphemes() {
|
||||
let mut p = page();
|
||||
write(&mut p, 0, 0, 9);
|
||||
p.append_grapheme(0, 0, 10).unwrap();
|
||||
p.append_grapheme(0, 0, 11).unwrap();
|
||||
let q = p.clone();
|
||||
assert!(q.rows[0].grapheme());
|
||||
assert_eq!(q.lookup_grapheme(0).unwrap(), [10, 11]);
|
||||
}
|
||||
#[test]
|
||||
fn page_clone_styles() {
|
||||
let mut p = page();
|
||||
for x in 0..10 {
|
||||
write(&mut p, x, 0, x as u32 + 1);
|
||||
p.set_style(x, 0, bold()).unwrap();
|
||||
}
|
||||
let q = p.clone();
|
||||
for x in 0..10 {
|
||||
assert_eq!(p.cells[x].style_id(), q.cells[x].style_id());
|
||||
assert_eq!(q.style(q.cells[x]), bold());
|
||||
}
|
||||
q.assert_integrity();
|
||||
}
|
||||
#[test]
|
||||
fn page_clone_from_shrink_columns() {
|
||||
let mut p = page();
|
||||
for y in 0..10 {
|
||||
write(&mut p, 1, y, y as u32);
|
||||
}
|
||||
let mut q = Page::init(Capacity {
|
||||
cols: 5,
|
||||
..p.capacity
|
||||
})
|
||||
.unwrap();
|
||||
q.clone_from(&p, 0, 10).unwrap();
|
||||
assert_eq!(q.size.cols, 5);
|
||||
for y in 0..10 {
|
||||
assert_eq!(cp(&q, 1, y), y as u32);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn page_clone_from_partial() {
|
||||
let mut p = page();
|
||||
for y in 0..10 {
|
||||
write(&mut p, 1, y, y as u32);
|
||||
}
|
||||
let mut q = page();
|
||||
q.clone_from(&p, 0, 5).unwrap();
|
||||
for y in 0..10 {
|
||||
assert_eq!(cp(&q, 1, y), if y < 5 { y as u32 } else { 0 });
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn page_clone_from_hyperlinks_exact_capacity() {
|
||||
let mut p = Page::init(Capacity {
|
||||
cols: 50,
|
||||
rows: 50,
|
||||
..Capacity::default()
|
||||
})
|
||||
.unwrap();
|
||||
let id = p.insert_hyperlink(link()).unwrap();
|
||||
for n in 0..p.hyperlink_capacity() {
|
||||
let x = n / 50;
|
||||
let y = n % 50;
|
||||
write(&mut p, x, y, 42);
|
||||
p.hyperlink_set.r#use(id).unwrap();
|
||||
p.set_hyperlink(x, y, id).unwrap();
|
||||
}
|
||||
assert_eq!(p.hyperlink_count(), p.hyperlink_capacity());
|
||||
let mut q = Page::init(p.capacity).unwrap();
|
||||
q.clone_from(&p, 0, 50).unwrap();
|
||||
assert_eq!(q.hyperlink_count(), p.hyperlink_count());
|
||||
}
|
||||
#[test]
|
||||
fn page_clone_from_graphemes() {
|
||||
let mut p = page();
|
||||
for y in 0..10 {
|
||||
write(&mut p, 1, y, y as u32 + 1);
|
||||
p.append_grapheme(1, y, 10).unwrap();
|
||||
}
|
||||
let mut q = page();
|
||||
q.clone_from(&p, 0, 10).unwrap();
|
||||
for y in 0..10 {
|
||||
p.clear_cells(y, 0, 10, false).unwrap();
|
||||
assert_eq!(cp(&q, 1, y), y as u32 + 1);
|
||||
assert_eq!(
|
||||
q.lookup_grapheme(q.cell_index(1, y).unwrap()).unwrap(),
|
||||
[10]
|
||||
);
|
||||
}
|
||||
q.assert_integrity();
|
||||
}
|
||||
#[test]
|
||||
fn page_clone_from_frees_dst_graphemes() {
|
||||
let p = page();
|
||||
let mut q = page();
|
||||
for y in 0..10 {
|
||||
write(&mut q, 1, y, 9);
|
||||
q.append_grapheme(1, y, 10).unwrap();
|
||||
}
|
||||
q.clone_from(&p, 0, 10).unwrap();
|
||||
assert_eq!(q.grapheme_count(), 0);
|
||||
for y in 0..10 {
|
||||
assert!(!q.rows[y].grapheme());
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn page_clone_row_from_partial() {
|
||||
let mut p = page();
|
||||
let mut q = page();
|
||||
for x in 0..10 {
|
||||
write(&mut p, x, 0, x as u32 + 1);
|
||||
}
|
||||
q.clone_partial_row_from(&p, 0, 0, 2, 2, 6).unwrap();
|
||||
for x in 0..10 {
|
||||
assert_eq!(
|
||||
cp(&q, x, 0),
|
||||
if (2..8).contains(&x) { x as u32 + 1 } else { 0 }
|
||||
);
|
||||
}
|
||||
assert_eq!(q.grapheme_count(), 0);
|
||||
assert!(!q.rows[0].grapheme());
|
||||
q.assert_integrity();
|
||||
}
|
||||
#[test]
|
||||
fn page_clone_row_from_partial_grapheme_in_non_copied_source_region() {
|
||||
let mut p = page();
|
||||
let mut q = page();
|
||||
for x in 0..10 {
|
||||
write(&mut p, x, 0, x as u32 + 1);
|
||||
}
|
||||
for x in [0, 9] {
|
||||
p.append_grapheme(x, 0, 10).unwrap();
|
||||
}
|
||||
q.clone_partial_row_from(&p, 0, 0, 2, 2, 6).unwrap();
|
||||
for x in 0..10 {
|
||||
assert_eq!(
|
||||
cp(&q, x, 0),
|
||||
if (2..8).contains(&x) { x as u32 + 1 } else { 0 }
|
||||
);
|
||||
}
|
||||
assert_eq!(q.grapheme_count(), 0);
|
||||
assert!(!q.rows[0].grapheme());
|
||||
q.assert_integrity();
|
||||
}
|
||||
#[test]
|
||||
fn page_clone_row_from_partial_grapheme_in_non_copied_dest_region() {
|
||||
let mut p = page();
|
||||
let mut q = page();
|
||||
for x in 0..10 {
|
||||
write(&mut p, x, 0, x as u32 + 1);
|
||||
}
|
||||
for x in 0..10 {
|
||||
write(&mut q, x, 0, 0xbb);
|
||||
}
|
||||
for x in [0, 9] {
|
||||
q.append_grapheme(x, 0, 10).unwrap();
|
||||
}
|
||||
q.clone_partial_row_from(&p, 0, 0, 2, 2, 6).unwrap();
|
||||
for x in 0..10 {
|
||||
assert_eq!(
|
||||
cp(&q, x, 0),
|
||||
if (2..8).contains(&x) {
|
||||
x as u32 + 1
|
||||
} else {
|
||||
0xbb
|
||||
}
|
||||
);
|
||||
}
|
||||
assert_eq!(q.grapheme_count(), 2);
|
||||
assert!(q.rows[0].grapheme());
|
||||
q.assert_integrity();
|
||||
}
|
||||
#[test]
|
||||
fn page_clone_row_from_partial_hyperlink_in_same_page_copy() {
|
||||
let mut p = page();
|
||||
for x in 0..10 {
|
||||
write(&mut p, x, 0, x as u32 + 1);
|
||||
}
|
||||
let id = p.insert_hyperlink(link()).unwrap();
|
||||
p.set_hyperlink(7, 0, id).unwrap();
|
||||
p.clone_cells_within(0, 1, 2, 2, 8 - 2).unwrap();
|
||||
for x in 0..10 {
|
||||
assert_eq!(
|
||||
cp(&p, x, 1),
|
||||
if (2..8).contains(&x) { x as u32 + 1 } else { 0 }
|
||||
);
|
||||
}
|
||||
assert_eq!(p.hyperlink_count(), 2);
|
||||
assert!(p.rows[1].hyperlink());
|
||||
p.assert_integrity();
|
||||
}
|
||||
#[test]
|
||||
fn page_clone_row_from_partial_hyperlink_in_same_page_omit() {
|
||||
let mut p = page();
|
||||
for x in 0..10 {
|
||||
write(&mut p, x, 0, x as u32 + 1);
|
||||
}
|
||||
let id = p.insert_hyperlink(link()).unwrap();
|
||||
p.set_hyperlink(7, 0, id).unwrap();
|
||||
p.clone_cells_within(0, 1, 2, 2, 6 - 2).unwrap();
|
||||
for x in 0..10 {
|
||||
assert_eq!(
|
||||
cp(&p, x, 1),
|
||||
if (2..6).contains(&x) { x as u32 + 1 } else { 0 }
|
||||
);
|
||||
}
|
||||
assert_eq!(p.hyperlink_count(), 1);
|
||||
assert!(!p.rows[1].hyperlink());
|
||||
p.assert_integrity();
|
||||
}
|
||||
#[test]
|
||||
fn page_move_cells_text_only() {
|
||||
let mut p = page();
|
||||
for x in 0..10 {
|
||||
write(&mut p, x, 0, x as u32 + 1);
|
||||
}
|
||||
p.move_cells(0, 0, 1, 0, 10).unwrap();
|
||||
for x in 0..10 {
|
||||
assert_eq!(cp(&p, x, 0), 0);
|
||||
assert_eq!(cp(&p, x, 1), x as u32 + 1);
|
||||
}
|
||||
p.assert_integrity();
|
||||
}
|
||||
#[test]
|
||||
fn page_move_cells_graphemes() {
|
||||
let mut p = page();
|
||||
for x in 0..10 {
|
||||
write(&mut p, x, 0, x as u32 + 1);
|
||||
p.append_grapheme(x, 0, 10).unwrap();
|
||||
}
|
||||
p.move_cells(0, 0, 1, 0, 10).unwrap();
|
||||
for x in 0..10 {
|
||||
assert_eq!(cp(&p, x, 0), 0);
|
||||
assert_eq!(cp(&p, x, 1), x as u32 + 1);
|
||||
assert_eq!(
|
||||
p.lookup_grapheme(p.cell_index(x, 1).unwrap()).unwrap(),
|
||||
[10]
|
||||
);
|
||||
}
|
||||
p.assert_integrity();
|
||||
}
|
||||
#[test]
|
||||
fn page_verify_integrity_graphemes_good() {
|
||||
let mut p = page();
|
||||
write(&mut p, 0, 0, 1);
|
||||
p.append_grapheme(0, 0, 10).unwrap();
|
||||
p.assert_integrity();
|
||||
}
|
||||
#[test]
|
||||
fn page_verify_integrity_grapheme_row_not_marked() {
|
||||
let mut p = page();
|
||||
write(&mut p, 0, 0, 1);
|
||||
p.append_grapheme(0, 0, 10).unwrap();
|
||||
p.rows[0].set_grapheme(false);
|
||||
assert_eq!(
|
||||
p.verify_integrity(),
|
||||
Err(IntegrityError::UnmarkedGraphemeRow)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn page_verify_integrity_styles_good() {
|
||||
let mut p = page();
|
||||
for x in 0..10 {
|
||||
write(&mut p, x, 0, x as u32 + 1);
|
||||
p.set_style(x, 0, bold()).unwrap();
|
||||
}
|
||||
p.assert_integrity();
|
||||
}
|
||||
#[test]
|
||||
fn page_verify_integrity_styles_ref_count_mismatch() {
|
||||
let mut p = page();
|
||||
for x in 0..10 {
|
||||
write(&mut p, x, 0, x as u32 + 1);
|
||||
p.set_style(x, 0, bold()).unwrap();
|
||||
}
|
||||
p.styles.release(p.cells[0].style_id()).unwrap();
|
||||
assert_eq!(
|
||||
p.verify_integrity(),
|
||||
Err(IntegrityError::MismatchedStyleRef)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn page_verify_integrity_zero_rows() {
|
||||
let mut p = page();
|
||||
p.size.rows = 0;
|
||||
assert_eq!(p.verify_integrity(), Err(IntegrityError::ZeroRowCount));
|
||||
}
|
||||
#[test]
|
||||
fn page_verify_integrity_zero_cols() {
|
||||
let mut p = page();
|
||||
p.size.cols = 0;
|
||||
assert_eq!(p.verify_integrity(), Err(IntegrityError::ZeroColCount));
|
||||
}
|
||||
#[test]
|
||||
fn page_exact_row_capacity_empty_rows() {
|
||||
let p = page();
|
||||
assert_eq!(
|
||||
p.exact_row_capacity(0, 5).unwrap(),
|
||||
Capacity {
|
||||
cols: 10,
|
||||
rows: 5,
|
||||
styles: 0,
|
||||
hyperlink_bytes: 0,
|
||||
grapheme_bytes: 0,
|
||||
string_bytes: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn page_exact_row_capacity_styles() {
|
||||
let mut p = page();
|
||||
assert_eq!(p.exact_row_capacity(0, 5).unwrap().styles, 0);
|
||||
p.set_style(0, 0, bold()).unwrap();
|
||||
assert_eq!(
|
||||
usize::from(p.exact_row_capacity(0, 5).unwrap().styles),
|
||||
style::Set::capacity_for_count(1)
|
||||
);
|
||||
p.set_style(1, 0, bold()).unwrap();
|
||||
assert_eq!(
|
||||
usize::from(p.exact_row_capacity(0, 5).unwrap().styles),
|
||||
style::Set::capacity_for_count(1)
|
||||
);
|
||||
p.set_style(
|
||||
2,
|
||||
0,
|
||||
Style {
|
||||
flags: Flags {
|
||||
italic: true,
|
||||
..Flags::default()
|
||||
},
|
||||
..Style::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
p.set_style(
|
||||
0,
|
||||
7,
|
||||
Style {
|
||||
flags: Flags {
|
||||
underline: ghostty_vt::sgr::Underline::Single,
|
||||
..Flags::default()
|
||||
},
|
||||
..Style::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
usize::from(p.exact_row_capacity(0, 5).unwrap().styles),
|
||||
style::Set::capacity_for_count(2)
|
||||
);
|
||||
assert_eq!(
|
||||
usize::from(p.exact_row_capacity(0, 10).unwrap().styles),
|
||||
style::Set::capacity_for_count(3)
|
||||
);
|
||||
clone_exact(&p, 5);
|
||||
}
|
||||
#[test]
|
||||
fn page_exact_row_capacity_single_style_clone() {
|
||||
let mut p = page();
|
||||
p.set_style(0, 0, bold()).unwrap();
|
||||
let q = clone_exact(&p, 1);
|
||||
assert_eq!(q.style(q.cells[0]), bold());
|
||||
}
|
||||
#[test]
|
||||
fn page_exact_row_capacity_styles_max_single_row() {
|
||||
let mut p = Page::init(Capacity {
|
||||
cols: u16::MAX,
|
||||
rows: 1,
|
||||
styles: u16::MAX,
|
||||
..Capacity::default()
|
||||
})
|
||||
.unwrap();
|
||||
for x in 0..1000 {
|
||||
p.set_style(
|
||||
x,
|
||||
0,
|
||||
Style {
|
||||
fg_color: Color::Rgb(Rgb {
|
||||
r: x as u8,
|
||||
g: (x >> 8) as u8,
|
||||
b: 0,
|
||||
}),
|
||||
..Style::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
assert_eq!(
|
||||
usize::from(p.exact_row_capacity(0, 1).unwrap().styles),
|
||||
style::Set::capacity_for_count(1000)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn page_exact_row_capacity_grapheme_bytes() {
|
||||
let mut p = page();
|
||||
assert_eq!(p.exact_row_capacity(0, 5).unwrap().grapheme_bytes, 0);
|
||||
for (x, cp0) in [(0, 97), (1, 101), (2, 111)] {
|
||||
write(&mut p, x, 0, cp0);
|
||||
p.append_grapheme(x, 0, 0x301).unwrap();
|
||||
assert_eq!(
|
||||
p.exact_row_capacity(0, 5).unwrap().grapheme_bytes,
|
||||
(x as u32 + 1) * 16
|
||||
);
|
||||
}
|
||||
p.append_grapheme(2, 0, 0x302).unwrap();
|
||||
p.append_grapheme(2, 0, 0x303).unwrap();
|
||||
write(&mut p, 0, 7, 120);
|
||||
p.append_grapheme(0, 7, 0x304).unwrap();
|
||||
assert_eq!(p.exact_row_capacity(0, 5).unwrap().grapheme_bytes, 48);
|
||||
assert_eq!(p.exact_row_capacity(0, 10).unwrap().grapheme_bytes, 64);
|
||||
clone_exact(&p, 5);
|
||||
}
|
||||
#[test]
|
||||
fn page_exact_row_capacity_grapheme_bytes_larger_than_chunk() {
|
||||
let mut p = page();
|
||||
write(&mut p, 0, 0, 97);
|
||||
for cp0 in 0x300..0x306 {
|
||||
p.append_grapheme(0, 0, cp0).unwrap();
|
||||
}
|
||||
assert_eq!(p.exact_row_capacity(0, 1).unwrap().grapheme_bytes, 32);
|
||||
clone_exact(&p, 1);
|
||||
}
|
||||
#[test]
|
||||
fn page_exact_row_capacity_hyperlinks() {
|
||||
let mut p = Page::init(Capacity {
|
||||
hyperlink_bytes: 32 * 48,
|
||||
string_bytes: 512,
|
||||
..page().capacity
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(p.exact_row_capacity(0, 5).unwrap().hyperlink_bytes, 0);
|
||||
let id = p.insert_hyperlink(link()).unwrap();
|
||||
p.set_hyperlink(0, 0, id).unwrap();
|
||||
let one = p.exact_row_capacity(0, 5).unwrap();
|
||||
assert_eq!(one.string_bytes, 32);
|
||||
assert_eq!(
|
||||
usize::from(one.hyperlink_bytes),
|
||||
hyperlink::Set::capacity_for_count(1) * 48
|
||||
);
|
||||
p.hyperlink_set.r#use(id).unwrap();
|
||||
p.set_hyperlink(1, 0, id).unwrap();
|
||||
assert_eq!(p.exact_row_capacity(0, 5).unwrap(), one);
|
||||
let id = p
|
||||
.insert_hyperlink(Hyperlink {
|
||||
id: HyperlinkId::Explicit(b"my-link-id".to_vec()),
|
||||
uri: b"https://other.example.org/path".to_vec(),
|
||||
})
|
||||
.unwrap();
|
||||
p.set_hyperlink(2, 0, id).unwrap();
|
||||
assert_eq!(p.exact_row_capacity(0, 5).unwrap().string_bytes, 96);
|
||||
let id = p
|
||||
.insert_hyperlink(Hyperlink {
|
||||
id: HyperlinkId::Implicit(99),
|
||||
uri: b"https://outside.example.com".to_vec(),
|
||||
})
|
||||
.unwrap();
|
||||
p.set_hyperlink(0, 7, id).unwrap();
|
||||
assert_eq!(p.exact_row_capacity(0, 5).unwrap().string_bytes, 96);
|
||||
assert_eq!(p.exact_row_capacity(0, 10).unwrap().string_bytes, 128);
|
||||
clone_exact(&p, 5);
|
||||
}
|
||||
#[test]
|
||||
fn page_exact_row_capacity_single_hyperlink_clone() {
|
||||
let mut p = page();
|
||||
let id = p.insert_hyperlink(link()).unwrap();
|
||||
p.set_hyperlink(0, 0, id).unwrap();
|
||||
let q = clone_exact(&p, 1);
|
||||
assert!(q.cells[0].hyperlink());
|
||||
}
|
||||
#[test]
|
||||
fn page_exact_row_capacity_hyperlink_map_capacity_for_many_cells() {
|
||||
let mut p = Page::init(Capacity {
|
||||
cols: 50,
|
||||
rows: 2,
|
||||
hyperlink_bytes: 32 * 48,
|
||||
string_bytes: 512,
|
||||
..Capacity::default()
|
||||
})
|
||||
.unwrap();
|
||||
let id = p.insert_hyperlink(link()).unwrap();
|
||||
for x in 0..50 {
|
||||
if x > 0 {
|
||||
p.hyperlink_set.r#use(id).unwrap();
|
||||
}
|
||||
p.set_hyperlink(x, 0, id).unwrap();
|
||||
}
|
||||
assert!(p.exact_row_capacity(0, 1).unwrap().hyperlink_bytes >= 4 * 48);
|
||||
let q = clone_exact(&p, 1);
|
||||
assert_eq!(q.hyperlink_count(), 50);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user