max / shop
- Co-Authored-By
- Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 files changed,
+335 insertions,
-21 deletions
| @@ -64,6 +64,26 @@ | |||
| 64 | 64 | Bar, | |
| 65 | 65 | } | |
| 66 | 66 | ||
| 67 | + | /// State delta since the last [`Grid::take_damage`] — the renderer's cue for | |
| 68 | + | /// which per-row instance caches to rotate, rebuild, or invalidate. | |
| 69 | + | #[derive(Debug, Clone, Default)] | |
| 70 | + | pub struct Damage { | |
| 71 | + | /// Rows the full screen scrolled up (positive) or down (negative). Only | |
| 72 | + | /// nonzero when the scroll region covered the entire screen — the | |
| 73 | + | /// renderer can rotate its per-row cache by this amount and skip | |
| 74 | + | /// re-emitting the shifted rows. Partial-region scrolls mark all affected | |
| 75 | + | /// rows dirty instead. | |
| 76 | + | pub scroll: i16, | |
| 77 | + | /// Rows whose cell contents changed. Includes the blank rows exposed by | |
| 78 | + | /// a scroll (i.e. after scroll-up-by-N the bottom N rows are dirty). | |
| 79 | + | pub dirty_rows: Vec<u16>, | |
| 80 | + | /// Alt-screen state toggled. The renderer should throw away its whole | |
| 81 | + | /// per-row cache and rebuild from `dirty_rows`. | |
| 82 | + | pub screen_swapped: bool, | |
| 83 | + | /// Grid was resized. Same effect as `screen_swapped` on the renderer. | |
| 84 | + | pub resized: bool, | |
| 85 | + | } | |
| 86 | + | ||
| 67 | 87 | /// Cursor state (position + deferred-wrap flag). | |
| 68 | 88 | #[derive(Copy, Clone, Debug, Default)] | |
| 69 | 89 | pub struct Cursor { | |
| @@ -93,6 +113,11 @@ | |||
| 93 | 113 | pending_bg: Color, | |
| 94 | 114 | pending_attrs: Attrs, | |
| 95 | 115 | pending_title: Option<String>, | |
| 116 | + | // Damage tracking — accumulated between take_damage() calls. | |
| 117 | + | row_dirty: Vec<bool>, | |
| 118 | + | pending_scroll: i16, | |
| 119 | + | pending_screen_swap: bool, | |
| 120 | + | pending_resize: bool, | |
| 96 | 121 | } | |
| 97 | 122 | ||
| 98 | 123 | impl Grid { | |
| @@ -119,6 +144,46 @@ | |||
| 119 | 144 | pending_bg: Color::Default, | |
| 120 | 145 | pending_attrs: Attrs::default(), | |
| 121 | 146 | pending_title: None, | |
| 147 | + | // Initial state: everything dirty so first render populates the | |
| 148 | + | // per-row cache. | |
| 149 | + | row_dirty: vec![true; rows as usize], | |
| 150 | + | pending_scroll: 0, | |
| 151 | + | pending_screen_swap: false, | |
| 152 | + | pending_resize: true, | |
| 153 | + | } | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | /// Drain accumulated changes since the last call. The renderer applies | |
| 157 | + | /// them (rotate cache, rebuild dirty rows, wipe on swap/resize) before | |
| 158 | + | /// emitting the frame. | |
| 159 | + | pub fn take_damage(&mut self) -> Damage { | |
| 160 | + | let scroll = std::mem::take(&mut self.pending_scroll); | |
| 161 | + | let screen_swapped = std::mem::take(&mut self.pending_screen_swap); | |
| 162 | + | let resized = std::mem::take(&mut self.pending_resize); | |
| 163 | + | let mut dirty_rows: Vec<u16> = Vec::new(); | |
| 164 | + | for (i, d) in self.row_dirty.iter_mut().enumerate() { | |
| 165 | + | if *d { | |
| 166 | + | dirty_rows.push(i as u16); | |
| 167 | + | *d = false; | |
| 168 | + | } | |
| 169 | + | } | |
| 170 | + | Damage { | |
| 171 | + | scroll, | |
| 172 | + | dirty_rows, | |
| 173 | + | screen_swapped, | |
| 174 | + | resized, | |
| 175 | + | } | |
| 176 | + | } | |
| 177 | + | ||
| 178 | + | fn mark_row_dirty(&mut self, row: u16) { | |
| 179 | + | if let Some(slot) = self.row_dirty.get_mut(row as usize) { | |
| 180 | + | *slot = true; | |
| 181 | + | } | |
| 182 | + | } | |
| 183 | + | ||
| 184 | + | fn mark_all_rows_dirty(&mut self) { | |
| 185 | + | for d in &mut self.row_dirty { | |
| 186 | + | *d = true; | |
| 122 | 187 | } | |
| 123 | 188 | } | |
| 124 | 189 | ||
| @@ -180,6 +245,10 @@ | |||
| 180 | 245 | self.cursor.row = self.cursor.row.min(rows - 1); | |
| 181 | 246 | self.cursor.col = self.cursor.col.min(cols - 1); | |
| 182 | 247 | self.cursor.wrap_next = false; | |
| 248 | + | // Resize invalidates any per-row cache; caller wipes on receipt. | |
| 249 | + | self.row_dirty = vec![true; rows as usize]; | |
| 250 | + | self.pending_resize = true; | |
| 251 | + | self.pending_scroll = 0; | |
| 183 | 252 | } | |
| 184 | 253 | ||
| 185 | 254 | fn cell_index(&self, row: u16, col: u16) -> usize { | |
| @@ -194,13 +263,15 @@ | |||
| 194 | 263 | self.cursor.col = 0; | |
| 195 | 264 | self.cursor.wrap_next = false; | |
| 196 | 265 | } | |
| 197 | - | let idx = self.cell_index(self.cursor.row, self.cursor.col); | |
| 266 | + | let row = self.cursor.row; | |
| 267 | + | let idx = self.cell_index(row, self.cursor.col); | |
| 198 | 268 | self.active_cells_mut()[idx] = Cell { | |
| 199 | 269 | c, | |
| 200 | 270 | fg: self.pending_fg, | |
| 201 | 271 | bg: self.pending_bg, | |
| 202 | 272 | attrs: self.pending_attrs, | |
| 203 | 273 | }; | |
| 274 | + | self.mark_row_dirty(row); | |
| 204 | 275 | if self.cursor.col + 1 >= self.cols { | |
| 205 | 276 | self.cursor.wrap_next = true; | |
| 206 | 277 | } else { | |
| @@ -377,6 +448,18 @@ | |||
| 377 | 448 | cells[r * cols + c] = Cell::default(); | |
| 378 | 449 | } | |
| 379 | 450 | } | |
| 451 | + | // Damage: if the region is the whole screen, tell the renderer to | |
| 452 | + | // rotate its cache instead of rebuilding every row. | |
| 453 | + | if self.scroll_top == 0 && self.scroll_bottom == self.rows - 1 { | |
| 454 | + | self.pending_scroll = self.pending_scroll.saturating_add(n as i16); | |
| 455 | + | for r in (self.rows as usize - n)..self.rows as usize { | |
| 456 | + | self.row_dirty[r] = true; | |
| 457 | + | } | |
| 458 | + | } else { | |
| 459 | + | for r in self.scroll_top..=self.scroll_bottom { | |
| 460 | + | self.mark_row_dirty(r); | |
| 461 | + | } | |
| 462 | + | } | |
| 380 | 463 | } | |
| 381 | 464 | ||
| 382 | 465 | fn scroll_down_in_region(&mut self, n: u16) { | |
| @@ -395,11 +478,22 @@ | |||
| 395 | 478 | cells[r * cols + c] = Cell::default(); | |
| 396 | 479 | } | |
| 397 | 480 | } | |
| 481 | + | if self.scroll_top == 0 && self.scroll_bottom == self.rows - 1 { | |
| 482 | + | self.pending_scroll = self.pending_scroll.saturating_sub(n as i16); | |
| 483 | + | for r in 0..n { | |
| 484 | + | self.row_dirty[r] = true; | |
| 485 | + | } | |
| 486 | + | } else { | |
| 487 | + | for r in self.scroll_top..=self.scroll_bottom { | |
| 488 | + | self.mark_row_dirty(r); | |
| 489 | + | } | |
| 490 | + | } | |
| 398 | 491 | } | |
| 399 | 492 | ||
| 400 | 493 | fn erase_line(&mut self, mode: u16) { | |
| 401 | 494 | let cols = self.cols as usize; | |
| 402 | - | let row_start = self.cursor.row as usize * cols; | |
| 495 | + | let row = self.cursor.row; | |
| 496 | + | let row_start = row as usize * cols; | |
| 403 | 497 | let col = self.cursor.col as usize; | |
| 404 | 498 | let cells = self.active_cells_mut(); | |
| 405 | 499 | let (from, to) = match mode { | |
| @@ -410,6 +504,7 @@ | |||
| 410 | 504 | for cell in &mut cells[from..to] { | |
| 411 | 505 | *cell = Cell::default(); | |
| 412 | 506 | } | |
| 507 | + | self.mark_row_dirty(row); | |
| 413 | 508 | } | |
| 414 | 509 | ||
| 415 | 510 | fn erase_display(&mut self, mode: u16) { | |
| @@ -425,6 +520,10 @@ | |||
| 425 | 520 | for cell in &mut cells[from..to] { | |
| 426 | 521 | *cell = Cell::default(); | |
| 427 | 522 | } | |
| 523 | + | // Erase display always touches at least the cursor row and (except | |
| 524 | + | // for mode-tail) rows around it. Cheap to just mark all — the cases | |
| 525 | + | // where only one row changes are minority. | |
| 526 | + | self.mark_all_rows_dirty(); | |
| 428 | 527 | } | |
| 429 | 528 | ||
| 430 | 529 | fn swap_alt(&mut self, to_alt: bool) { | |
| @@ -443,6 +542,8 @@ | |||
| 443 | 542 | self.on_alt = false; | |
| 444 | 543 | self.cursor = self.saved_main_cursor; | |
| 445 | 544 | } | |
| 545 | + | self.pending_screen_swap = true; | |
| 546 | + | self.mark_all_rows_dirty(); | |
| 446 | 547 | } | |
| 447 | 548 | ||
| 448 | 549 | fn set_cursor(&mut self, row: u16, col: u16) { |
| @@ -108,6 +108,10 @@ | |||
| 108 | 108 | atlas: GlyphAtlas, | |
| 109 | 109 | shaper: Shaper, | |
| 110 | 110 | cache: HashMap<GlyphId, Option<CachedGlyph>>, | |
| 111 | + | /// Per-row cache of "cheap-to-recompute" instance data. Positions get | |
| 112 | + | /// computed at draw time from row_idx + cell_advance, so scroll can | |
| 113 | + | /// just rotate this vec. | |
| 114 | + | row_cache: Vec<Vec<CachedCell>>, | |
| 111 | 115 | } | |
| 112 | 116 | ||
| 113 | 117 | #[derive(Clone, Copy)] | |
| @@ -117,6 +121,19 @@ | |||
| 117 | 121 | top: i32, | |
| 118 | 122 | } | |
| 119 | 123 | ||
| 124 | + | /// Per-glyph metadata cached for a live cell. Position is NOT stored — it's | |
| 125 | + | /// derived from (row_idx, col, scale, pad) at draw time so the same cache | |
| 126 | + | /// entry stays valid across scroll rotations and doesn't need re-computing | |
| 127 | + | /// on scale changes. | |
| 128 | + | #[derive(Clone, Copy)] | |
| 129 | + | struct CachedCell { | |
| 130 | + | col: u16, | |
| 131 | + | slot: AtlasSlot, | |
| 132 | + | left: i32, | |
| 133 | + | top: i32, | |
| 134 | + | color: [f32; 4], | |
| 135 | + | } | |
| 136 | + | ||
| 120 | 137 | impl TextRenderer { | |
| 121 | 138 | pub fn new( | |
| 122 | 139 | device: &wgpu::Device, | |
| @@ -275,9 +292,174 @@ | |||
| 275 | 292 | atlas, | |
| 276 | 293 | shaper, | |
| 277 | 294 | cache: HashMap::new(), | |
| 295 | + | row_cache: Vec::new(), | |
| 278 | 296 | }) | |
| 279 | 297 | } | |
| 280 | 298 | ||
| 299 | + | /// Ensure the per-row cache has `rows` slots. Sizing up appends empty | |
| 300 | + | /// rows (renderer treats them as no glyphs — caller marks them dirty). | |
| 301 | + | /// Sizing down truncates. Cache contents are otherwise preserved. | |
| 302 | + | pub fn ensure_rows(&mut self, rows: u16) { | |
| 303 | + | self.row_cache.resize(rows as usize, Vec::new()); | |
| 304 | + | } | |
| 305 | + | ||
| 306 | + | /// Wipe the per-row cache — for alt-screen swap or full grid resize. | |
| 307 | + | pub fn clear_rows(&mut self) { | |
| 308 | + | for row in &mut self.row_cache { | |
| 309 | + | row.clear(); | |
| 310 | + | } | |
| 311 | + | } | |
| 312 | + | ||
| 313 | + | /// Rotate the per-row cache to reflect a fullscreen scroll. Positive = | |
| 314 | + | /// scrolled up (contents move up N rows, blank appears at bottom). | |
| 315 | + | /// Caller is responsible for calling `update_row` on the newly-blank | |
| 316 | + | /// rows to fill in their fresh contents. | |
| 317 | + | pub fn scroll(&mut self, delta: i16) { | |
| 318 | + | if delta == 0 || self.row_cache.is_empty() { | |
| 319 | + | return; | |
| 320 | + | } | |
| 321 | + | let n = delta.unsigned_abs() as usize; | |
| 322 | + | let n = n.min(self.row_cache.len()); | |
| 323 | + | if delta > 0 { | |
| 324 | + | self.row_cache.rotate_left(n); | |
| 325 | + | } else { | |
| 326 | + | self.row_cache.rotate_right(n); | |
| 327 | + | } | |
| 328 | + | } | |
| 329 | + | ||
| 330 | + | /// Replace one row's cached glyph batch with a fresh build from the | |
| 331 | + | /// supplied cells. `cells` yields `(col, char, fg_color)` for each cell | |
| 332 | + | /// that should draw a glyph (blanks skipped by caller). | |
| 333 | + | pub fn update_row( | |
| 334 | + | &mut self, | |
| 335 | + | queue: &wgpu::Queue, | |
| 336 | + | row: u16, | |
| 337 | + | cells: impl IntoIterator<Item = (u16, char, [f32; 4])>, | |
| 338 | + | ) { | |
| 339 | + | let Some(slot) = self.row_cache.get_mut(row as usize) else { | |
| 340 | + | return; | |
| 341 | + | }; | |
| 342 | + | slot.clear(); | |
| 343 | + | // We hold &mut self.row_cache[row], so we can't touch self.cache | |
| 344 | + | // through &mut self. Take the entries out, work with local refs. | |
| 345 | + | let atlas = &mut self.atlas; | |
| 346 | + | let cache = &mut self.cache; | |
| 347 | + | let shaper = &mut self.shaper; | |
| 348 | + | for (col, c, color) in cells { | |
| 349 | + | let glyph_id = shaper.glyph_id_for(c); | |
| 350 | + | let cached = match cache.get(&glyph_id).copied() { | |
| 351 | + | Some(v) => v, | |
| 352 | + | None => { | |
| 353 | + | let entry = shaper.rasterize(glyph_id).and_then(|r| { | |
| 354 | + | atlas.upload(queue, r.width, r.height, &r.bitmap).map(|s| CachedGlyph { | |
| 355 | + | slot: s, | |
| 356 | + | left: r.placement_left, | |
| 357 | + | top: r.placement_top, | |
| 358 | + | }) | |
| 359 | + | }); | |
| 360 | + | cache.insert(glyph_id, entry); | |
| 361 | + | entry | |
| 362 | + | } | |
| 363 | + | }; | |
| 364 | + | if let Some(g) = cached | |
| 365 | + | && g.slot.px[0] > 0 | |
| 366 | + | && g.slot.px[1] > 0 | |
| 367 | + | { | |
| 368 | + | slot.push(CachedCell { | |
| 369 | + | col, | |
| 370 | + | slot: g.slot, | |
| 371 | + | left: g.left, | |
| 372 | + | top: g.top, | |
| 373 | + | color, | |
| 374 | + | }); | |
| 375 | + | } | |
| 376 | + | } | |
| 377 | + | } | |
| 378 | + | ||
| 379 | + | /// Draw one frame from the per-row cache + supplied fills. Positions | |
| 380 | + | /// are computed here so scroll rotation and scale changes don't | |
| 381 | + | /// invalidate the cache. | |
| 382 | + | pub fn draw_cached( | |
| 383 | + | &mut self, | |
| 384 | + | device: &wgpu::Device, | |
| 385 | + | queue: &wgpu::Queue, | |
| 386 | + | view: &wgpu::TextureView, | |
| 387 | + | encoder: &mut wgpu::CommandEncoder, | |
| 388 | + | fills: &[BgFill], | |
| 389 | + | pad_x: f32, | |
| 390 | + | pad_y: f32, | |
| 391 | + | cell_w: f32, | |
| 392 | + | cell_h: f32, | |
| 393 | + | ) { | |
| 394 | + | let ascent = self.shaper.ascent(); | |
| 395 | + | let glyph_count: usize = self.row_cache.iter().map(Vec::len).sum(); | |
| 396 | + | let mut instances: Vec<Instance> = Vec::with_capacity(fills.len() + glyph_count); | |
| 397 | + | let solid_uv = self.atlas.solid_uv; | |
| 398 | + | ||
| 399 | + | for fill in fills { | |
| 400 | + | instances.push(Instance { | |
| 401 | + | pos: [fill.x, fill.y], | |
| 402 | + | size: [fill.w, fill.h], | |
| 403 | + | uv_min: solid_uv, | |
| 404 | + | uv_max: solid_uv, | |
| 405 | + | color: fill.color, | |
| 406 | + | }); | |
| 407 | + | } | |
| 408 | + | ||
| 409 | + | for (row_idx, row) in self.row_cache.iter().enumerate() { | |
| 410 | + | let row_y = pad_y + row_idx as f32 * cell_h; | |
| 411 | + | for cell in row { | |
| 412 | + | let cell_x = pad_x + cell.col as f32 * cell_w; | |
| 413 | + | instances.push(Instance { | |
| 414 | + | pos: [ | |
| 415 | + | cell_x + cell.left as f32, | |
| 416 | + | row_y + ascent - cell.top as f32, | |
| 417 | + | ], | |
| 418 | + | size: [cell.slot.px[0] as f32, cell.slot.px[1] as f32], | |
| 419 | + | uv_min: cell.slot.uv_min, | |
| 420 | + | uv_max: cell.slot.uv_max, | |
| 421 | + | color: cell.color, | |
| 422 | + | }); | |
| 423 | + | } | |
| 424 | + | } | |
| 425 | + | ||
| 426 | + | if instances.is_empty() { | |
| 427 | + | return; | |
| 428 | + | } | |
| 429 | + | let needed = instances.len() as u64; | |
| 430 | + | if needed > self.instance_cap { | |
| 431 | + | let new_cap = needed.next_power_of_two(); | |
| 432 | + | self.instance_buf = device.create_buffer(&wgpu::BufferDescriptor { | |
| 433 | + | label: Some("shop.text.instances"), | |
| 434 | + | size: new_cap * std::mem::size_of::<Instance>() as u64, | |
| 435 | + | usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, | |
| 436 | + | mapped_at_creation: false, | |
| 437 | + | }); | |
| 438 | + | self.instance_cap = new_cap; | |
| 439 | + | } | |
| 440 | + | queue.write_buffer(&self.instance_buf, 0, bytemuck::cast_slice(&instances)); | |
| 441 | + | ||
| 442 | + | let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { | |
| 443 | + | label: Some("shop.text.cached"), | |
| 444 | + | color_attachments: &[Some(wgpu::RenderPassColorAttachment { | |
| 445 | + | view, | |
| 446 | + | resolve_target: None, | |
| 447 | + | ops: wgpu::Operations { | |
| 448 | + | load: wgpu::LoadOp::Load, | |
| 449 | + | store: wgpu::StoreOp::Store, | |
| 450 | + | }, | |
| 451 | + | depth_slice: None, | |
| 452 | + | })], | |
| 453 | + | ..Default::default() | |
| 454 | + | }); | |
| 455 | + | pass.set_pipeline(&self.pipeline); | |
| 456 | + | pass.set_bind_group(0, &self.bind_group, &[]); | |
| 457 | + | pass.set_vertex_buffer(0, self.quad_buf.slice(..)); | |
| 458 | + | pass.set_vertex_buffer(1, self.instance_buf.slice(..)); | |
| 459 | + | pass.set_index_buffer(self.index_buf.slice(..), wgpu::IndexFormat::Uint16); | |
| 460 | + | pass.draw_indexed(0..6, 0, 0..instances.len() as u32); | |
| 461 | + | } | |
| 462 | + | ||
| 281 | 463 | /// Draw a frame: solid-color fills first (bg + cursor), glyph cells second. | |
| 282 | 464 | /// All emitted as a single instanced draw call using the atlas's reserved | |
| 283 | 465 | /// solid-white block for the fill quads. |
| @@ -15,7 +15,7 @@ | |||
| 15 | 15 | use kitty_graphics as kgp; | |
| 16 | 16 | use shop_grid::{Color as GridColor, CursorShape, Grid}; | |
| 17 | 17 | use shop_pty::{Pty, PtySize}; | |
| 18 | - | use shop_render::{BgFill, CellDraw, ImagePlacement, ImageRenderer, TextRenderer}; | |
| 18 | + | use shop_render::{BgFill, ImagePlacement, ImageRenderer, TextRenderer}; | |
| 19 | 19 | use shop_wayland::{ | |
| 20 | 20 | Capability, CompositorHandler, CompositorState, Connection, OutputHandler, OutputState, | |
| 21 | 21 | Pending, ProvidesRegistryState, QueueHandle, RegistryState, SeatHandler, SeatState, | |
| @@ -550,22 +550,50 @@ | |||
| 550 | 550 | ..Default::default() | |
| 551 | 551 | }); | |
| 552 | 552 | } | |
| 553 | - | // Build one batch per frame: bg fills (per-cell backgrounds + cursor) | |
| 554 | - | // then glyph cells. All positions in PHYSICAL px (surface uniform). | |
| 553 | + | // Damage flow: apply the grid's pending changes to the per-row cache, | |
| 554 | + | // rebuild only dirty rows, then draw from cache. | |
| 555 | 555 | let s = app.scale as f32; | |
| 556 | 556 | let cell_w_px = CELL_ADVANCE * s; | |
| 557 | 557 | let cell_h_px = CELL_HEIGHT * s; | |
| 558 | + | let pad_x_px = PAD_X * s; | |
| 559 | + | let pad_y_px = PAD_Y * s; | |
| 560 | + | let damage = app.grid.take_damage(); | |
| 561 | + | app.text.ensure_rows(app.grid.rows()); | |
| 562 | + | if damage.screen_swapped || damage.resized { | |
| 563 | + | app.text.clear_rows(); | |
| 564 | + | } | |
| 565 | + | if damage.scroll != 0 { | |
| 566 | + | app.text.scroll(damage.scroll); | |
| 567 | + | } | |
| 568 | + | for &row in &damage.dirty_rows { | |
| 569 | + | app.text.update_row( | |
| 570 | + | &app.queue, | |
| 571 | + | row, | |
| 572 | + | app.grid.row(row).iter().enumerate().filter_map(|(col, cell)| { | |
| 573 | + | let c = cell.c; | |
| 574 | + | if c == ' ' || c == '\0' { | |
| 575 | + | return None; | |
| 576 | + | } | |
| 577 | + | let mut fg = resolve_color(cell.fg, DEFAULT_FG); | |
| 578 | + | let mut bg = resolve_color(cell.bg, DEFAULT_BG); | |
| 579 | + | if cell.attrs.reverse { | |
| 580 | + | std::mem::swap(&mut fg, &mut bg); | |
| 581 | + | } | |
| 582 | + | let _ = bg; // fg only for glyph cache; bg is handled via fills below | |
| 583 | + | Some((col as u16, c, fg)) | |
| 584 | + | }), | |
| 585 | + | ); | |
| 586 | + | } | |
| 587 | + | ||
| 588 | + | // Fills (bg + underline + cursor) are cheap to fully rebuild each frame; | |
| 589 | + | // they change on cursor phase toggle anyway. | |
| 558 | 590 | let mut fills: Vec<BgFill> = Vec::new(); | |
| 559 | - | let mut cells: Vec<CellDraw> = Vec::with_capacity( | |
| 560 | - | app.grid.rows() as usize * app.grid.cols() as usize, | |
| 561 | - | ); | |
| 562 | 591 | let cursor = app.grid.cursor(); | |
| 563 | 592 | let cursor_shape = app.grid.cursor_shape(); | |
| 564 | - | ||
| 565 | 593 | for r in 0..app.grid.rows() { | |
| 566 | - | let y = (PAD_Y + r as f32 * CELL_HEIGHT) * s; | |
| 594 | + | let y = pad_y_px + r as f32 * cell_h_px; | |
| 567 | 595 | for (col, cell) in app.grid.row(r).iter().enumerate() { | |
| 568 | - | let x = (PAD_X + col as f32 * CELL_ADVANCE) * s; | |
| 596 | + | let x = pad_x_px + col as f32 * cell_w_px; | |
| 569 | 597 | let mut fg = resolve_color(cell.fg, DEFAULT_FG); | |
| 570 | 598 | let mut bg = resolve_color(cell.bg, DEFAULT_BG); | |
| 571 | 599 | if cell.attrs.reverse { | |
| @@ -589,16 +617,12 @@ | |||
| 589 | 617 | color: fg, | |
| 590 | 618 | }); | |
| 591 | 619 | } | |
| 592 | - | let c = cell.c; | |
| 593 | - | if c != ' ' && c != '\0' { | |
| 594 | - | cells.push(CellDraw { c, x, y, color: fg }); | |
| 595 | - | } | |
| 596 | 620 | } | |
| 597 | 621 | } | |
| 598 | 622 | ||
| 599 | 623 | if cursor.visible { | |
| 600 | - | let cx = (PAD_X + cursor.col as f32 * CELL_ADVANCE) * s; | |
| 601 | - | let cy = (PAD_Y + cursor.row as f32 * CELL_HEIGHT) * s; | |
| 624 | + | let cx = pad_x_px + cursor.col as f32 * cell_w_px; | |
| 625 | + | let cy = pad_y_px + cursor.row as f32 * cell_h_px; | |
| 602 | 626 | let base = if app.cursor_phase { | |
| 603 | 627 | CURSOR_COLOR_ON | |
| 604 | 628 | } else { | |
| @@ -624,10 +648,17 @@ | |||
| 624 | 648 | }); | |
| 625 | 649 | } | |
| 626 | 650 | ||
| 627 | - | if !fills.is_empty() || !cells.is_empty() { | |
| 628 | - | app.text | |
| 629 | - | .draw_frame(&app.device, &app.queue, &view, &mut encoder, &fills, &cells); | |
| 630 | - | } | |
| 651 | + | app.text.draw_cached( | |
| 652 | + | &app.device, | |
| 653 | + | &app.queue, | |
| 654 | + | &view, | |
| 655 | + | &mut encoder, | |
| 656 | + | &fills, | |
| 657 | + | pad_x_px, | |
| 658 | + | pad_y_px, | |
| 659 | + | cell_w_px, | |
| 660 | + | cell_h_px, | |
| 661 | + | ); | |
| 631 | 662 | // Draw any live kitty-graphics image on top of the text layer. | |
| 632 | 663 | if let Some(placement) = app.image_placement { | |
| 633 | 664 | app.images |