| 109 |
109 |
|
on_alt: bool,
|
| 110 |
110 |
|
main_origin: u16,
|
| 111 |
111 |
|
alt_origin: u16,
|
|
112 |
+ |
/// Rotation within the current scroll region. Independent from
|
|
113 |
+ |
/// main/alt_origin. Non-zero only when the active screen is in a
|
|
114 |
+ |
/// non-fullscreen region (DECSTBM); enforced by unroll on transition.
|
|
115 |
+ |
region_origin: u16,
|
| 112 |
116 |
|
cursor: Cursor,
|
| 113 |
117 |
|
cursor_shape: CursorShape,
|
| 114 |
118 |
|
saved_main_cursor: Cursor,
|
| 139 |
143 |
|
on_alt: false,
|
| 140 |
144 |
|
main_origin: 0,
|
| 141 |
145 |
|
alt_origin: 0,
|
|
146 |
+ |
region_origin: 0,
|
| 142 |
147 |
|
cursor: Cursor {
|
| 143 |
148 |
|
visible: true,
|
| 144 |
149 |
|
..Cursor::default()
|
| 246 |
251 |
|
|
| 247 |
252 |
|
/// Physical byte offset for the start of logical row `r`.
|
| 248 |
253 |
|
fn row_start(&self, r: u16) -> usize {
|
| 249 |
|
- |
let phys = (self.active_origin() as u32 + r as u32) % self.rows as u32;
|
|
254 |
+ |
let phys = if self.is_partial_region()
|
|
255 |
+ |
&& r >= self.scroll_top
|
|
256 |
+ |
&& r <= self.scroll_bottom
|
|
257 |
+ |
{
|
|
258 |
+ |
// In partial region: active_origin is guaranteed 0 by unroll on
|
|
259 |
+ |
// transition, so we rotate only within the region.
|
|
260 |
+ |
let region_size = (self.scroll_bottom - self.scroll_top + 1) as u32;
|
|
261 |
+ |
let region_r = (r - self.scroll_top) as u32;
|
|
262 |
+ |
let phys_in_region = (self.region_origin as u32 + region_r) % region_size;
|
|
263 |
+ |
self.scroll_top as u32 + phys_in_region
|
|
264 |
+ |
} else {
|
|
265 |
+ |
(self.active_origin() as u32 + r as u32) % self.rows as u32
|
|
266 |
+ |
};
|
| 250 |
267 |
|
phys as usize * self.cols as usize
|
| 251 |
268 |
|
}
|
| 252 |
269 |
|
|
|
270 |
+ |
fn is_partial_region(&self) -> bool {
|
|
271 |
+ |
self.scroll_top != 0 || self.scroll_bottom != self.rows - 1
|
|
272 |
+ |
}
|
|
273 |
+ |
|
|
274 |
+ |
/// Rotate the active screen's cells so `active_origin` becomes 0.
|
|
275 |
+ |
/// Cheap: one `slice::rotate_left`. Called before entering partial-region
|
|
276 |
+ |
/// mode so rows outside the region are at logical=physical positions.
|
|
277 |
+ |
fn unroll_active_ring(&mut self) {
|
|
278 |
+ |
let origin = self.active_origin();
|
|
279 |
+ |
if origin == 0 {
|
|
280 |
+ |
return;
|
|
281 |
+ |
}
|
|
282 |
+ |
let cols = self.cols as usize;
|
|
283 |
+ |
let cells = self.active_cells_mut();
|
|
284 |
+ |
cells.rotate_left(origin as usize * cols);
|
|
285 |
+ |
if self.on_alt {
|
|
286 |
+ |
self.alt_origin = 0;
|
|
287 |
+ |
} else {
|
|
288 |
+ |
self.main_origin = 0;
|
|
289 |
+ |
}
|
|
290 |
+ |
}
|
|
291 |
+ |
|
|
292 |
+ |
/// Rotate the current partial region so `region_origin` becomes 0.
|
|
293 |
+ |
/// Called before exiting partial-region mode (or entering a different
|
|
294 |
+ |
/// region) so region contents are back at logical positions.
|
|
295 |
+ |
fn unroll_region(&mut self) {
|
|
296 |
+ |
if self.region_origin == 0 {
|
|
297 |
+ |
return;
|
|
298 |
+ |
}
|
|
299 |
+ |
let cols = self.cols as usize;
|
|
300 |
+ |
let top = self.scroll_top as usize;
|
|
301 |
+ |
let region_len = (self.scroll_bottom - self.scroll_top + 1) as usize * cols;
|
|
302 |
+ |
let shift = self.region_origin as usize * cols;
|
|
303 |
+ |
let cells = self.active_cells_mut();
|
|
304 |
+ |
cells[top * cols..top * cols + region_len].rotate_left(shift);
|
|
305 |
+ |
self.region_origin = 0;
|
|
306 |
+ |
}
|
|
307 |
+ |
|
| 253 |
308 |
|
/// Advance the active screen's ring origin. Positive `n` = scroll up
|
| 254 |
309 |
|
/// (logical row 0 shows what was logical row `n`); negative = scroll
|
| 255 |
310 |
|
/// down. Blanking of newly-exposed rows is the caller's job.
|
| 310 |
365 |
|
);
|
| 311 |
366 |
|
self.main_origin = 0;
|
| 312 |
367 |
|
self.alt_origin = 0;
|
|
368 |
+ |
self.region_origin = 0;
|
| 313 |
369 |
|
self.cols = cols;
|
| 314 |
370 |
|
self.rows = rows;
|
| 315 |
371 |
|
self.scroll_top = 0;
|
| 505 |
561 |
|
}
|
| 506 |
562 |
|
|
| 507 |
563 |
|
fn scroll_up_in_region(&mut self, n: u16) {
|
| 508 |
|
- |
let n = n.min(self.scroll_bottom - self.scroll_top + 1);
|
|
564 |
+ |
let region_size = self.scroll_bottom - self.scroll_top + 1;
|
|
565 |
+ |
let n = n.min(region_size);
|
| 509 |
566 |
|
if n == 0 {
|
| 510 |
567 |
|
return;
|
| 511 |
568 |
|
}
|
| 512 |
|
- |
if self.scroll_top == 0 && self.scroll_bottom == self.rows - 1 {
|
| 513 |
|
- |
// Fullscreen fast path: O(1) origin shift + blank the newly-
|
| 514 |
|
- |
// exposed rows (which are physically the old top-of-ring).
|
|
569 |
+ |
if !self.is_partial_region() {
|
|
570 |
+ |
// Fullscreen fast path: O(1) origin shift + blank exposed rows.
|
| 515 |
571 |
|
let old_origin = self.active_origin();
|
| 516 |
572 |
|
self.advance_origin(n as i32);
|
| 517 |
573 |
|
for k in 0..n {
|
| 523 |
579 |
|
self.row_dirty[r as usize] = true;
|
| 524 |
580 |
|
}
|
| 525 |
581 |
|
} else {
|
| 526 |
|
- |
// Partial region: memcpy via logical indices (ring-mapped).
|
| 527 |
|
- |
self.memcpy_scroll_up_partial(n);
|
| 528 |
|
- |
for r in self.scroll_top..=self.scroll_bottom {
|
|
582 |
+ |
// Partial region: ring-rotate within the region. active_origin is
|
|
583 |
+ |
// guaranteed 0 in partial mode (unrolled on DECSTBM entry).
|
|
584 |
+ |
let rs = region_size as u32;
|
|
585 |
+ |
let old_region_origin = self.region_origin as u32;
|
|
586 |
+ |
self.region_origin = ((old_region_origin + n as u32) % rs) as u16;
|
|
587 |
+ |
for k in 0..n {
|
|
588 |
+ |
let phys_in_region = (old_region_origin + k as u32) % rs;
|
|
589 |
+ |
self.blank_physical_row(self.scroll_top + phys_in_region as u16);
|
|
590 |
+ |
}
|
|
591 |
+ |
// Partial-region scrolls don't propagate to the renderer's cache
|
|
592 |
+ |
// rotation; mark exposed rows dirty for rebuild.
|
|
593 |
+ |
for r in (self.scroll_bottom + 1 - n)..=self.scroll_bottom {
|
| 529 |
594 |
|
self.mark_row_dirty(r);
|
| 530 |
595 |
|
}
|
| 531 |
596 |
|
}
|
| 532 |
597 |
|
}
|
| 533 |
598 |
|
|
| 534 |
599 |
|
fn scroll_down_in_region(&mut self, n: u16) {
|
| 535 |
|
- |
let n = n.min(self.scroll_bottom - self.scroll_top + 1);
|
|
600 |
+ |
let region_size = self.scroll_bottom - self.scroll_top + 1;
|
|
601 |
+ |
let n = n.min(region_size);
|
| 536 |
602 |
|
if n == 0 {
|
| 537 |
603 |
|
return;
|
| 538 |
604 |
|
}
|
| 539 |
|
- |
if self.scroll_top == 0 && self.scroll_bottom == self.rows - 1 {
|
|
605 |
+ |
if !self.is_partial_region() {
|
| 540 |
606 |
|
self.advance_origin(-(n as i32));
|
| 541 |
607 |
|
for k in 0..n {
|
| 542 |
608 |
|
self.blank_physical_row((self.active_origin() as u32 + k as u32) as u16 % self.rows);
|
| 546 |
612 |
|
self.row_dirty[r as usize] = true;
|
| 547 |
613 |
|
}
|
| 548 |
614 |
|
} else {
|
| 549 |
|
- |
self.memcpy_scroll_down_partial(n);
|
| 550 |
|
- |
for r in self.scroll_top..=self.scroll_bottom {
|
|
615 |
+ |
let rs = region_size as u32;
|
|
616 |
+ |
let new_region_origin = ((self.region_origin as u32 + rs - n as u32) % rs) as u16;
|
|
617 |
+ |
self.region_origin = new_region_origin;
|
|
618 |
+ |
for k in 0..n {
|
|
619 |
+ |
let phys_in_region = (new_region_origin as u32 + k as u32) % rs;
|
|
620 |
+ |
self.blank_physical_row(self.scroll_top + phys_in_region as u16);
|
|
621 |
+ |
}
|
|
622 |
+ |
for r in self.scroll_top..(self.scroll_top + n) {
|
| 551 |
623 |
|
self.mark_row_dirty(r);
|
| 552 |
624 |
|
}
|
| 553 |
625 |
|
}
|
| 554 |
626 |
|
}
|
| 555 |
627 |
|
|
| 556 |
|
- |
fn memcpy_scroll_up_partial(&mut self, n: u16) {
|
| 557 |
|
- |
let cols = self.cols as usize;
|
| 558 |
|
- |
let top = self.scroll_top;
|
| 559 |
|
- |
let bot = self.scroll_bottom;
|
| 560 |
|
- |
// Collect physical row ranges first so we can borrow cells mutably.
|
| 561 |
|
- |
for r in top..=(bot - n) {
|
| 562 |
|
- |
let src = self.row_start(r + n);
|
| 563 |
|
- |
let dst = self.row_start(r);
|
| 564 |
|
- |
let cells = self.active_cells_mut();
|
| 565 |
|
- |
cells.copy_within(src..src + cols, dst);
|
| 566 |
|
- |
}
|
| 567 |
|
- |
for r in (bot - n + 1)..=bot {
|
| 568 |
|
- |
self.blank_logical_row(r);
|
| 569 |
|
- |
}
|
| 570 |
|
- |
}
|
| 571 |
|
- |
|
| 572 |
|
- |
fn memcpy_scroll_down_partial(&mut self, n: u16) {
|
| 573 |
|
- |
let cols = self.cols as usize;
|
| 574 |
|
- |
let top = self.scroll_top;
|
| 575 |
|
- |
let bot = self.scroll_bottom;
|
| 576 |
|
- |
for r in (top + n..=bot).rev() {
|
| 577 |
|
- |
let src = self.row_start(r - n);
|
| 578 |
|
- |
let dst = self.row_start(r);
|
| 579 |
|
- |
let cells = self.active_cells_mut();
|
| 580 |
|
- |
cells.copy_within(src..src + cols, dst);
|
| 581 |
|
- |
}
|
| 582 |
|
- |
for r in top..(top + n) {
|
| 583 |
|
- |
self.blank_logical_row(r);
|
| 584 |
|
- |
}
|
| 585 |
|
- |
}
|
| 586 |
|
- |
|
| 587 |
628 |
|
fn erase_line(&mut self, mode: u16) {
|
| 588 |
629 |
|
let cols = self.cols as usize;
|
| 589 |
630 |
|
let row = self.cursor.row;
|
| 644 |
685 |
|
if self.on_alt == to_alt {
|
| 645 |
686 |
|
return;
|
| 646 |
687 |
|
}
|
|
688 |
+ |
// region_origin belongs to the currently-active screen; unroll before
|
|
689 |
+ |
// switching so the other screen starts with region_origin = 0.
|
|
690 |
+ |
self.unroll_region();
|
| 647 |
691 |
|
if to_alt {
|
| 648 |
692 |
|
self.saved_main_cursor = self.cursor;
|
| 649 |
693 |
|
self.on_alt = true;
|
| 866 |
910 |
|
self.scroll_down_in_region(param1(params, 1));
|
| 867 |
911 |
|
}
|
| 868 |
912 |
|
('r', false) => {
|
|
913 |
+ |
// DECSTBM: transition-safe. If we're currently in a partial
|
|
914 |
+ |
// region with a rotated region_origin, unroll it back to
|
|
915 |
+ |
// logical order first. If the new region is partial, unroll
|
|
916 |
+ |
// the fullscreen ring so outside-region rows sit at their
|
|
917 |
+ |
// logical physical positions. Unroll preserves logical
|
|
918 |
+ |
// contents so the renderer's per-row cache stays valid — no
|
|
919 |
+ |
// need to mark rows dirty here.
|
|
920 |
+ |
self.unroll_region();
|
| 869 |
921 |
|
let (top, bot) = param2(params, (1, self.rows));
|
| 870 |
922 |
|
self.scroll_top = top.saturating_sub(1).min(self.rows - 1);
|
| 871 |
923 |
|
self.scroll_bottom = bot.saturating_sub(1).min(self.rows - 1);
|
|
924 |
+ |
if self.is_partial_region() {
|
|
925 |
+ |
self.unroll_active_ring();
|
|
926 |
+ |
}
|
| 872 |
927 |
|
self.cursor.row = 0;
|
| 873 |
928 |
|
self.cursor.col = 0;
|
| 874 |
929 |
|
}
|