diff --git a/src/font/shape.zig b/src/font/shape.zig index 0423d0ed1..3721c63a6 100644 --- a/src/font/shape.zig +++ b/src/font/shape.zig @@ -44,10 +44,7 @@ pub const Cell = struct { /// this cell is available in the text run. This glyph index is only /// valid for a given GroupCache and FontIndex that was used to create /// the runs. - /// - /// If this is null then this is an empty cell. If there are styles - /// then those should be applied but there is no glyph to render. - glyph_index: ?u32, + glyph_index: u32, }; /// Options for shapers. diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index 185ff1aca..085f913d3 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -437,15 +437,6 @@ pub const Shaper = struct { // wait for that. if (cell_offset.cluster > cluster) break :pad; - // If we have a gap between clusters then we need to - // add empty cells to the buffer. - for (cell_offset.cluster + 1..cluster) |x| { - try self.cell_buf.append(self.alloc, .{ - .x = @intCast(x), - .glyph_index = null, - }); - } - cell_offset = .{ .cluster = cluster }; } @@ -463,25 +454,6 @@ pub const Shaper = struct { } } - // If our last cell doesn't match our last cluster then we have - // a left-replaced ligature that needs to have spaces appended - // so that cells retain their background colors. - if (self.cell_buf.items.len > 0) pad: { - const last_cell = self.cell_buf.items[self.cell_buf.items.len - 1]; - const last_cp = state.codepoints.items[state.codepoints.items.len - 1]; - if (last_cell.x == last_cp.cluster) break :pad; - assert(last_cell.x < last_cp.cluster); - - // We need to go back to the last matched cluster and add - // padding up to there. - for (last_cell.x + 1..last_cp.cluster + 1) |x| { - try self.cell_buf.append(self.alloc, .{ - .x = @intCast(x), - .glyph_index = null, - }); - } - } - return self.cell_buf.items; } diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index 6dd520ad4..53efeec54 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -150,7 +150,7 @@ pub const Shaper = struct { // Convert all our info/pos to cells and set it. self.cell_buf.clearRetainingCapacity(); - for (info, pos, 0..) |info_v, pos_v, i| { + for (info, pos) |info_v, pos_v| { // If our cluster changed then we've moved to a new cell. if (info_v.cluster != cell_offset.cluster) cell_offset = .{ .cluster = info_v.cluster, @@ -174,48 +174,6 @@ pub const Shaper = struct { cell_offset.y += pos_v.y_advance; } - // Determine the width of the cell. To do this, we have to - // find the next cluster that has been shaped. This tells us how - // many cells this glyph replaced (i.e. for ligatures). For example - // in some fonts "!=" turns into a single glyph from the component - // parts "!" and "=" so this cell width would be "2" despite - // only having a single glyph. - // - // Many fonts replace ligature cells with space so that this always - // is one (e.g. Fira Code, JetBrains Mono, etc). Some do not - // (e.g. Monaspace). - const cell_width = width: { - if (i + 1 < info.len) { - // We may have to go through multiple glyphs because - // multiple can be replaced. e.g. "===" - for (info[i + 1 ..]) |next_info_v| { - if (next_info_v.cluster != info_v.cluster) { - // We do a saturating sub here because for RTL - // text, the next cluster can be less than the - // current cluster. We don't really support RTL - // currently so we do this to prevent an underflow - // but it isn't correct generally. - break :width next_info_v.cluster -| info_v.cluster; - } - } - } - - // If we reached the end then our width is our max cluster - // minus this one. - const max = run.offset + run.cells; - break :width max - info_v.cluster; - }; - if (cell_width > 1) { - // To make the renderer implementations simpler, we convert - // the extra spaces for width to blank cells. - for (1..cell_width) |j| { - try self.cell_buf.append(self.alloc, .{ - .x = @intCast(info_v.cluster + j), - .glyph_index = null, - }); - } - } - // const i = self.cell_buf.items.len - 1; // log.warn("i={} info={} pos={} cell={}", .{ i, info_v, pos_v, self.cell_buf.items[i] }); } diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 20c14cbad..9340ce5d0 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -2209,7 +2209,7 @@ fn rebuildCells( var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null); var y: terminal.size.CellCountInt = screen.pages.rows; while (row_it.next()) |row| { - y = y - 1; + y -= 1; if (!rebuild) { // Only rebuild if we are doing a full rebuild or this row is dirty. @@ -2255,82 +2255,317 @@ fn rebuildCells( }, } - // Split our row into runs and shape each one. - var iter = self.font_shaper.runIterator( + // Iterator of of runs for shaping. + var run_iter = self.font_shaper.runIterator( self.font_grid, screen, row, row_selection, if (shape_cursor) screen.cursor.x else null, ); - while (try iter.next(self.alloc)) |run| { - // Try to read the cells from the shaping cache if we can. - const shaper_cells = self.font_shaper_cache.get(run) orelse cache: { - const cells = try self.font_shaper.shape(run); + var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc); + var shaper_cells: ?[]const font.shape.Cell = null; + var shaper_cells_i: usize = 0; - // Try to cache them. If caching fails for any reason we continue - // because it is just a performance optimization, not a correctness - // issue. - self.font_shaper_cache.put(self.alloc, run, cells) catch |err| { - log.warn("error caching font shaping results err={}", .{err}); - }; + const row_cells = row.cells(.all); - // The cells we get from direct shaping are always owned by - // the shaper and valid until the next shaping call so we can - // just return them. - break :cache cells; - }; - - for (shaper_cells) |shaper_cell| { - // The shaper can emit null glyphs representing the right half - // of wide characters, we don't need to do anything with them. - if (shaper_cell.glyph_index == null) continue; - - const coord: terminal.Coordinate = .{ - .x = shaper_cell.x, - .y = y, - }; - - // If this cell falls within our preedit range then we - // skip this because preedits are setup separately. - if (preedit_range) |range| { - if (range.y == coord.y and - coord.x >= range.x[0] and - coord.x <= range.x[1]) + for (row_cells, 0..) |*cell, x| { + // If this cell falls within our preedit range then we + // skip this because preedits are setup separately. + if (preedit_range) |range| { + if (range.y == y) { + if (x >= range.x[0] and + x <= range.x[1]) { continue; } - } - // It this cell is within our hint range then we need to - // underline it. - const cell: terminal.Pin = cell: { - var copy = row; - copy.x = coord.x; - break :cell copy; - }; + // After exiting the preedit range we need to catch + // the run position up because of the missed cells. + if (x == range.x[1] + 1) { + while (shaper_run) |run| { + if (run.offset + run.cells > x) break; + shaper_run = try run_iter.next(self.alloc); + shaper_cells = null; + shaper_cells_i = 0; + } + if (shaper_run) |run| { + // Try to read the cells from the shaping cache if we can. + shaper_cells = self.font_shaper_cache.get(run) orelse cache: { + // Otherwise we have to shape them. + const cells = try self.font_shaper.shape(run); - if (self.updateCell( - screen, - cell, - if (link_match_set.contains(screen, cell)) - .single - else - null, - color_palette, - shaper_cell, - run, - coord, - )) |update| { - assert(update); - } else |err| { - log.warn("error building cell, will be invalid x={} y={}, err={}", .{ - coord.x, - coord.y, - err, - }); + // Try to cache them. If caching fails for any reason we + // continue because it is just a performance optimization, + // not a correctness issue. + self.font_shaper_cache.put( + self.alloc, + run, + cells, + ) catch |err| { + log.warn( + "error caching font shaping results err={}", + .{err}, + ); + }; + + // The cells we get from direct shaping are always owned + // by the shaper and valid until the next shaping call so + // we can safely use them. + break :cache cells; + }; + } + if (shaper_cells) |cells| { + while (cells[shaper_cells_i].x < x) { + shaper_cells_i += 1; + } + } + } } } + + const wide = cell.wide; + + const style = row.style(cell); + + const cell_pin: terminal.Pin = cell: { + var copy = row; + copy.x = @intCast(x); + break :cell copy; + }; + + // True if this cell is selected + const selected: bool = if (screen.selection) |sel| + sel.contains(screen, .{ + .page = row.page, + .y = row.y, + .x = @intCast( + // Spacer tails should show the selection + // state of the wide cell they belong to. + if (wide == .spacer_tail) + x -| 1 + else + x, + ), + }) + else + false; + + const bg_style = style.bg(cell, color_palette); + const fg_style = style.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color; + + // The final background color for the cell. + const bg = bg: { + if (selected) { + break :bg if (self.config.invert_selection_fg_bg) + if (style.flags.inverse) + // Cell is selected with invert selection fg/bg + // enabled, and the cell has the inverse style + // flag, so they cancel out and we get the normal + // bg color. + bg_style + else + // If it doesn't have the inverse style + // flag then we use the fg color instead. + fg_style + else + // If we don't have invert selection fg/bg set then we + // just use the selection background if set, otherwise + // the default fg color. + break :bg self.config.selection_background orelse self.foreground_color; + } + + // Not selected + break :bg if (style.flags.inverse != isCovering(cell.codepoint())) + // Two cases cause us to invert (use the fg color as the bg) + // - The "inverse" style flag. + // - A "covering" glyph; we use fg for bg in that case to + // help make sure that padding extension works correctly. + // If one of these is true (but not the other) + // then we use the fg style color for the bg. + fg_style + else + // Otherwise they cancel out. + bg_style; + }; + + const fg = fg: { + if (selected and !self.config.invert_selection_fg_bg) { + // If we don't have invert selection fg/bg set + // then we just use the selection foreground if + // set, otherwise the default bg color. + break :fg self.config.selection_foreground orelse self.background_color; + } + + // Whether we need to use the bg color as our fg color: + // - Cell is inverted and not selected + // - Cell is selected and not inverted + // Note: if selected then invert sel fg / bg must be + // false since we separately handle it if true above. + break :fg if (style.flags.inverse != selected) + bg_style orelse self.background_color + else + fg_style; + }; + + // Foreground alpha for this cell. + const alpha: u8 = if (style.flags.faint) 175 else 255; + + // If the cell has a background color, set it. + if (bg) |rgb| { + // Determine our background alpha. If we have transparency configured + // then this is dynamic depending on some situations. This is all + // in an attempt to make transparency look the best for various + // situations. See inline comments. + const bg_alpha: u8 = bg_alpha: { + const default: u8 = 255; + + if (self.config.background_opacity >= 1) break :bg_alpha default; + + // If we're selected, we do not apply background opacity + if (selected) break :bg_alpha default; + + // If we're reversed, do not apply background opacity + if (style.flags.inverse) break :bg_alpha default; + + // If we have a background and its not the default background + // then we apply background opacity + if (style.bg(cell, color_palette) != null and !rgb.eql(self.background_color)) { + break :bg_alpha default; + } + + // We apply background opacity. + var bg_alpha: f64 = @floatFromInt(default); + bg_alpha *= self.config.background_opacity; + bg_alpha = @ceil(bg_alpha); + break :bg_alpha @intFromFloat(bg_alpha); + }; + + self.cells.bgCell(y, x).* = .{ + rgb.r, rgb.g, rgb.b, bg_alpha, + }; + } + + // If the invisible flag is set on this cell then we + // don't need to render any foreground elements, so + // we just skip all glyphs with this x coordinate. + // + // NOTE: This behavior matches xterm. Some other terminal + // emulators, e.g. Alacritty, still render text decorations + // and only make the text itself invisible. The decision + // has been made here to match xterm's behavior for this. + if (style.flags.invisible) { + continue; + } + + // Give links a single underline, unless they already have + // an underline, in which case use a double underline to + // distinguish them. + const underline: terminal.Attribute.Underline = if (link_match_set.contains(screen, cell_pin)) + if (style.flags.underline == .single) + .double + else + .single + else + style.flags.underline; + + // We draw underlines first so that they layer underneath text. + // This improves readability when a colored underline is used + // which intersects parts of the text (descenders). + if (underline != .none) self.addUnderline( + @intCast(x), + @intCast(y), + underline, + style.underlineColor(color_palette) orelse fg, + alpha, + ) catch |err| { + log.warn( + "error adding underline to cell, will be invalid x={} y={}, err={}", + .{ x, y, err }, + ); + }; + + // If we're at or past the end of our shaper run then + // we need to get the next run from the run iterator. + if (shaper_cells != null and shaper_cells_i >= shaper_cells.?.len) { + shaper_run = try run_iter.next(self.alloc); + shaper_cells = null; + shaper_cells_i = 0; + } + + // If we haven't shaped this run yet, do so. + if (shaper_cells == null) if (shaper_run) |run| { + // Try to read the cells from the shaping cache if we can. + shaper_cells = self.font_shaper_cache.get(run) orelse cache: { + // Otherwise we have to shape them. + const cells = try self.font_shaper.shape(run); + + // Try to cache them. If caching fails for any reason we + // continue because it is just a performance optimization, + // not a correctness issue. + self.font_shaper_cache.put( + self.alloc, + run, + cells, + ) catch |err| { + log.warn( + "error caching font shaping results err={}", + .{err}, + ); + }; + + // The cells we get from direct shaping are always owned + // by the shaper and valid until the next shaping call so + // we can safely use them. + break :cache cells; + }; + }; + + // NOTE: An assumption is made here that a single cell will never + // be present in more than one shaper run. If that assumption is + // violated, this logic breaks. + if (shaper_cells) |cells| glyphs: { + // If there are no shaper cells for this run, ignore it. + // This can occur for runs of empty cells, and is fine. + if (cells.len == 0) break :glyphs; + + // If we encounter a shaper cell to the left of the current + // cell then we have some problems. This logic relies on x + // position monotonically increasing. + assert(cells[shaper_cells_i].x >= x); + + while (shaper_cells_i < cells.len and cells[shaper_cells_i].x == x) : ({ + shaper_cells_i += 1; + }) { + self.addGlyph( + @intCast(x), + @intCast(y), + cell_pin, + cells[shaper_cells_i], + shaper_run.?, + fg, + alpha, + ) catch |err| { + log.warn( + "error adding glyph to cell, will be invalid x={} y={}, err={}", + .{ x, y, err }, + ); + }; + } + } + + // Finally, draw a strikethrough if necessary. + if (style.flags.strikethrough) self.addStrikethrough( + @intCast(x), + @intCast(y), + fg, + alpha, + ) catch |err| { + log.warn( + "error adding strikethrough to cell, will be invalid x={} y={}, err={}", + .{ x, y, err }, + ); + }; } } @@ -2422,252 +2657,133 @@ fn rebuildCells( // }); } -fn updateCell( +/// Add an underline decoration to the specified cell +fn addUnderline( self: *Metal, - screen: *const terminal.Screen, + x: terminal.size.CellCountInt, + y: terminal.size.CellCountInt, + style: terminal.Attribute.Underline, + color: terminal.color.RGB, + alpha: u8, +) !void { + const sprite: font.Sprite = switch (style) { + .none => unreachable, + .single => .underline, + .double => .underline_double, + .dotted => .underline_dotted, + .dashed => .underline_dashed, + .curly => .underline_curly, + }; + + const render = try self.font_grid.renderGlyph( + self.alloc, + font.sprite_index, + @intFromEnum(sprite), + .{ + .cell_width = 1, + .grid_metrics = self.grid_metrics, + }, + ); + + try self.cells.add(self.alloc, .underline, .{ + .mode = .fg, + .grid_pos = .{ @intCast(x), @intCast(y) }, + .constraint_width = 1, + .color = .{ color.r, color.g, color.b, alpha }, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, + .bearings = .{ + @intCast(render.glyph.offset_x), + @intCast(render.glyph.offset_y), + }, + }); +} + +/// Add a strikethrough decoration to the specified cell +fn addStrikethrough( + self: *Metal, + x: terminal.size.CellCountInt, + y: terminal.size.CellCountInt, + color: terminal.color.RGB, + alpha: u8, +) !void { + const render = try self.font_grid.renderGlyph( + self.alloc, + font.sprite_index, + @intFromEnum(font.Sprite.strikethrough), + .{ + .cell_width = 1, + .grid_metrics = self.grid_metrics, + }, + ); + + try self.cells.add(self.alloc, .strikethrough, .{ + .mode = .fg, + .grid_pos = .{ @intCast(x), @intCast(y) }, + .constraint_width = 1, + .color = .{ color.r, color.g, color.b, alpha }, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, + .bearings = .{ + @intCast(render.glyph.offset_x), + @intCast(render.glyph.offset_y), + }, + }); +} + +// Add a glyph to the specified cell. +fn addGlyph( + self: *Metal, + x: terminal.size.CellCountInt, + y: terminal.size.CellCountInt, cell_pin: terminal.Pin, - cell_underline: ?terminal.Attribute.Underline, - palette: *const terminal.color.Palette, shaper_cell: font.shape.Cell, shaper_run: font.shape.TextRun, - coord: terminal.Coordinate, -) !bool { - const BgFg = struct { - /// Background is optional because in un-inverted mode - /// it may just be equivalent to the default background in - /// which case we do nothing to save on GPU render time. - bg: ?terminal.color.RGB, - - /// Fg is always set to some color, though we may not render - /// any fg if the cell is empty or has no attributes like - /// underline. - fg: terminal.color.RGB, - }; - - // True if this cell is selected - const selected: bool = if (screen.selection) |sel| - sel.contains(screen, cell_pin) - else - false; - + color: terminal.color.RGB, + alpha: u8, +) !void { const rac = cell_pin.rowAndCell(); const cell = rac.cell; - const style = cell_pin.style(cell); - const underline = cell_underline orelse style.flags.underline; - // The colors for the cell. - const colors: BgFg = colors: { - // The normal cell result - const cell_res: BgFg = if (!style.flags.inverse) .{ - // In normal mode, background and fg match the cell. We - // un-optionalize the fg by defaulting to our fg color. - .bg = style.bg(cell, palette), - .fg = style.fg(palette, self.config.bold_is_bright) orelse self.foreground_color, - } else .{ - // In inverted mode, the background MUST be set to something - // (is never null) so it is either the fg or default fg. The - // fg is either the bg or default background. - .bg = style.fg(palette, self.config.bold_is_bright) orelse self.foreground_color, - .fg = style.bg(cell, palette) orelse self.background_color, - }; + // Render + const render = try self.font_grid.renderGlyph( + self.alloc, + shaper_run.font_index, + shaper_cell.glyph_index, + .{ + .grid_metrics = self.grid_metrics, + .thicken = self.config.font_thicken, + }, + ); - // If we are selected, we our colors are just inverted fg/bg - const selection_res: ?BgFg = if (selected) .{ - .bg = if (self.config.invert_selection_fg_bg) - cell_res.fg - else - self.config.selection_background orelse self.foreground_color, - .fg = if (self.config.invert_selection_fg_bg) - cell_res.bg orelse self.background_color - else - self.config.selection_foreground orelse self.background_color, - } else null; + // If the glyph is 0 width or height, it will be invisible + // when drawn, so don't bother adding it to the buffer. + if (render.glyph.width == 0 or render.glyph.height == 0) { + return; + } - // If the cell is "invisible" then we just make fg = bg so that - // the cell is transparent but still copy-able. - const res: BgFg = selection_res orelse cell_res; - if (style.flags.invisible) { - break :colors .{ - .bg = res.bg, - .fg = res.bg orelse self.background_color, - }; - } - - // If our cell has a covering glyph, then our bg is set to our fg - // so that padding extension works correctly. - if (!selected and isCovering(cell.codepoint())) { - break :colors .{ - .bg = res.fg, - .fg = res.fg, - }; - } - - break :colors res; + const mode: mtl_shaders.CellText.Mode = switch (try fgMode( + render.presentation, + cell_pin, + )) { + .normal => .fg, + .color => .fg_color, + .constrained => .fg_constrained, + .powerline => .fg_powerline, }; - // Alpha multiplier - const alpha: u8 = if (style.flags.faint) 175 else 255; - - // If the cell has a background, we always draw it. - if (colors.bg) |rgb| { - // Determine our background alpha. If we have transparency configured - // then this is dynamic depending on some situations. This is all - // in an attempt to make transparency look the best for various - // situations. See inline comments. - const bg_alpha: u8 = bg_alpha: { - const default: u8 = 255; - - if (self.config.background_opacity >= 1) break :bg_alpha default; - - // If we're selected, we do not apply background opacity - if (selected) break :bg_alpha default; - - // If we're reversed, do not apply background opacity - if (style.flags.inverse) break :bg_alpha default; - - // If we have a background and its not the default background - // then we apply background opacity - if (style.bg(cell, palette) != null and !rgb.eql(self.background_color)) { - break :bg_alpha default; - } - - // We apply background opacity. - var bg_alpha: f64 = @floatFromInt(default); - bg_alpha *= self.config.background_opacity; - bg_alpha = @ceil(bg_alpha); - break :bg_alpha @intFromFloat(bg_alpha); - }; - - self.cells.bgCell(coord.y, coord.x).* = .{ - rgb.r, rgb.g, rgb.b, bg_alpha, - }; - if (cell.gridWidth() > 1 and coord.x < self.cells.size.columns - 1) { - self.cells.bgCell(coord.y, coord.x + 1).* = .{ - rgb.r, rgb.g, rgb.b, bg_alpha, - }; - } - } - - // If the cell has an underline, draw it before the character glyph, - // so that it layers underneath instead of overtop, since that can - // make text difficult to read. - if (underline != .none) { - const sprite: font.Sprite = switch (underline) { - .none => unreachable, - .single => .underline, - .double => .underline_double, - .dotted => .underline_dotted, - .dashed => .underline_dashed, - .curly => .underline_curly, - }; - - const render = try self.font_grid.renderGlyph( - self.alloc, - font.sprite_index, - @intFromEnum(sprite), - .{ - .cell_width = 1, - .grid_metrics = self.grid_metrics, - }, - ); - - const color = style.underlineColor(palette) orelse colors.fg; - - var gpu_cell: mtl_cell.Key.underline.CellType() = .{ - .mode = .fg, - .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, - .constraint_width = 1, - .color = .{ color.r, color.g, color.b, alpha }, - .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, - .glyph_size = .{ render.glyph.width, render.glyph.height }, - .bearings = .{ - @intCast(render.glyph.offset_x), - @intCast(render.glyph.offset_y), - }, - }; - try self.cells.add(self.alloc, .underline, gpu_cell); - // If it's a wide cell we need to underline the right half as well. - if (cell.gridWidth() > 1 and coord.x < self.cells.size.columns - 1) { - gpu_cell.grid_pos[0] = @intCast(coord.x + 1); - try self.cells.add(self.alloc, .underline, gpu_cell); - } - } - - // If the shaper cell has a glyph, draw it. - if (shaper_cell.glyph_index) |glyph_index| glyph: { - // Render - const render = try self.font_grid.renderGlyph( - self.alloc, - shaper_run.font_index, - glyph_index, - .{ - .grid_metrics = self.grid_metrics, - .thicken = self.config.font_thicken, - }, - ); - - // If the glyph is 0 width or height, it will be invisible - // when drawn, so don't bother adding it to the buffer. - if (render.glyph.width == 0 or render.glyph.height == 0) { - break :glyph; - } - - const mode: mtl_shaders.CellText.Mode = switch (try fgMode( - render.presentation, - cell_pin, - )) { - .normal => .fg, - .color => .fg_color, - .constrained => .fg_constrained, - .powerline => .fg_powerline, - }; - - try self.cells.add(self.alloc, .text, .{ - .mode = mode, - .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, - .constraint_width = cell.gridWidth(), - .color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha }, - .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, - .glyph_size = .{ render.glyph.width, render.glyph.height }, - .bearings = .{ - @intCast(render.glyph.offset_x + shaper_cell.x_offset), - @intCast(render.glyph.offset_y + shaper_cell.y_offset), - }, - }); - } - - if (style.flags.strikethrough) { - const render = try self.font_grid.renderGlyph( - self.alloc, - font.sprite_index, - @intFromEnum(font.Sprite.strikethrough), - .{ - .cell_width = 1, - .grid_metrics = self.grid_metrics, - }, - ); - - var gpu_cell: mtl_cell.Key.strikethrough.CellType() = .{ - .mode = .fg, - .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, - .constraint_width = 1, - .color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha }, - .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, - .glyph_size = .{ render.glyph.width, render.glyph.height }, - .bearings = .{ - @intCast(render.glyph.offset_x), - @intCast(render.glyph.offset_y), - }, - }; - try self.cells.add(self.alloc, .strikethrough, gpu_cell); - // If it's a wide cell we need to strike through the right half as well. - if (cell.gridWidth() > 1 and coord.x < self.cells.size.columns - 1) { - gpu_cell.grid_pos[0] = @intCast(coord.x + 1); - try self.cells.add(self.alloc, .strikethrough, gpu_cell); - } - } - - return true; + try self.cells.add(self.alloc, .text, .{ + .mode = mode, + .grid_pos = .{ @intCast(x), @intCast(y) }, + .constraint_width = cell.gridWidth(), + .color = .{ color.r, color.g, color.b, alpha }, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, + .bearings = .{ + @intCast(render.glyph.offset_x + shaper_cell.x_offset), + @intCast(render.glyph.offset_y + shaper_cell.y_offset), + }, + }); } fn addCursor( diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 5281a3c7f..54835d097 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -1255,29 +1255,10 @@ pub fn rebuildCells( } // Build each cell - var row_it = screen.pages.rowIterator(.right_down, .{ .viewport = .{} }, null); - var y: terminal.size.CellCountInt = 0; + var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null); + var y: terminal.size.CellCountInt = screen.pages.rows; while (row_it.next()) |row| { - defer y += 1; - - // Our selection value is only non-null if this selection happens - // to contain this row. This selection value will be set to only be - // the selection that contains this row. This way, if the selection - // changes but not for this line, we don't invalidate the cache. - const selection = sel: { - const sel = screen.selection orelse break :sel null; - const pin = screen.pages.pin(.{ .viewport = .{ .y = y } }) orelse - break :sel null; - break :sel sel.containedRow(screen, pin) orelse null; - }; - - // See Metal.zig - const cursor_row = if (cursor_style_) |cursor_style| - cursor_style == .block and - screen.viewportIsBottom() and - y == screen.cursor.y - else - false; + y -= 1; // True if we want to do font shaping around the cursor. We want to // do font shaping as long as the cursor is enabled. @@ -1287,7 +1268,7 @@ pub fn rebuildCells( // If this is the row with our cursor, then we may have to modify // the cell with the cursor. const start_i: usize = self.cells.items.len; - defer if (cursor_row) { + defer if (shape_cursor and cursor_style_ == .block) { const x = screen.cursor.x; const wide = row.cells(.all)[x].wide; const min_x = switch (wide) { @@ -1310,6 +1291,15 @@ pub fn rebuildCells( } }; + // We need to get this row's selection if there is one for proper + // run splitting. + const row_selection = sel: { + const sel = screen.selection orelse break :sel null; + const pin = screen.pages.pin(.{ .viewport = .{ .y = y } }) orelse + break :sel null; + break :sel sel.containedRow(screen, pin) orelse null; + }; + // On primary screen, we still apply vertical padding extension // under certain conditions we feel are safe. This helps make some // scenarios look better while avoiding scenarios we know do NOT look @@ -1332,79 +1322,346 @@ pub fn rebuildCells( }, } - // Split our row into runs and shape each one. - var iter = self.font_shaper.runIterator( + // Iterator of of runs for shaping. + var run_iter = self.font_shaper.runIterator( self.font_grid, screen, row, - selection, + row_selection, if (shape_cursor) screen.cursor.x else null, ); - while (try iter.next(self.alloc)) |run| { - // Try to read the cells from the shaping cache if we can. - const shaper_cells = self.font_shaper_cache.get(run) orelse cache: { - const cells = try self.font_shaper.shape(run); + var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc); + var shaper_cells: ?[]const font.shape.Cell = null; + var shaper_cells_i: usize = 0; - // Try to cache them. If caching fails for any reason we continue - // because it is just a performance optimization, not a correctness - // issue. - self.font_shaper_cache.put(self.alloc, run, cells) catch |err| { - log.warn("error caching font shaping results err={}", .{err}); - }; + const row_cells = row.cells(.all); - // The cells we get from direct shaping are always owned by - // the shaper and valid until the next shaping call so we can - // just return them. - break :cache cells; - }; - - for (shaper_cells) |shaper_cell| { - // The shaper can emit null glyphs representing the right half - // of wide characters, we don't need to do anything with them. - if (shaper_cell.glyph_index == null) continue; - - // If this cell falls within our preedit range then we skip it. - // We do this so we don't have conflicting data on the same - // cell. - if (preedit_range) |range| { - if (range.y == y and - shaper_cell.x >= range.x[0] and - shaper_cell.x <= range.x[1]) + for (row_cells, 0..) |*cell, x| { + // If this cell falls within our preedit range then we + // skip this because preedits are setup separately. + if (preedit_range) |range| { + if (range.y == y) { + if (x >= range.x[0] and + x <= range.x[1]) { continue; } - } - // It this cell is within our hint range then we need to - // underline it. - const cell: terminal.Pin = cell: { - var copy = row; - copy.x = shaper_cell.x; - break :cell copy; - }; + // After exiting the preedit range we need to catch + // the run position up because of the missed cells. + if (x == range.x[1] + 1) { + while (shaper_run) |run| { + if (run.offset + run.cells > x) break; + shaper_run = try run_iter.next(self.alloc); + shaper_cells = null; + shaper_cells_i = 0; + } + if (shaper_run) |run| { + // Try to read the cells from the shaping cache if we can. + shaper_cells = self.font_shaper_cache.get(run) orelse cache: { + // Otherwise we have to shape them. + const cells = try self.font_shaper.shape(run); - if (self.updateCell( - screen, - cell, - if (link_match_set.orderedContains(screen, cell)) - .single - else - null, - color_palette, - shaper_cell, - run, - shaper_cell.x, - y, - )) |update| { - assert(update); - } else |err| { - log.warn("error building cell, will be invalid x={} y={}, err={}", .{ - shaper_cell.x, - y, - err, - }); + // Try to cache them. If caching fails for any reason we + // continue because it is just a performance optimization, + // not a correctness issue. + self.font_shaper_cache.put( + self.alloc, + run, + cells, + ) catch |err| { + log.warn( + "error caching font shaping results err={}", + .{err}, + ); + }; + + // The cells we get from direct shaping are always owned + // by the shaper and valid until the next shaping call so + // we can safely use them. + break :cache cells; + }; + } + if (shaper_cells) |cells| { + while (cells[shaper_cells_i].x < x) { + shaper_cells_i += 1; + } + } + } } } + + const wide = cell.wide; + + const style = row.style(cell); + + const cell_pin: terminal.Pin = cell: { + var copy = row; + copy.x = @intCast(x); + break :cell copy; + }; + + // True if this cell is selected + const selected: bool = if (screen.selection) |sel| + sel.contains(screen, .{ + .page = row.page, + .y = row.y, + .x = @intCast( + // Spacer tails should show the selection + // state of the wide cell they belong to. + if (wide == .spacer_tail) + x -| 1 + else + x, + ), + }) + else + false; + + const bg_style = style.bg(cell, color_palette); + const fg_style = style.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color; + + // The final background color for the cell. + const bg = bg: { + if (selected) { + break :bg if (self.config.invert_selection_fg_bg) + if (style.flags.inverse) + // Cell is selected with invert selection fg/bg + // enabled, and the cell has the inverse style + // flag, so they cancel out and we get the normal + // bg color. + bg_style + else + // If it doesn't have the inverse style + // flag then we use the fg color instead. + fg_style + else + // If we don't have invert selection fg/bg set then we + // just use the selection background if set, otherwise + // the default fg color. + break :bg self.config.selection_background orelse self.foreground_color; + } + + // Not selected + break :bg if (style.flags.inverse != isCovering(cell.codepoint())) + // Two cases cause us to invert (use the fg color as the bg) + // - The "inverse" style flag. + // - A "covering" glyph; we use fg for bg in that case to + // help make sure that padding extension works correctly. + // If one of these is true (but not the other) + // then we use the fg style color for the bg. + fg_style + else + // Otherwise they cancel out. + bg_style; + }; + + const fg = fg: { + if (selected and !self.config.invert_selection_fg_bg) { + // If we don't have invert selection fg/bg set + // then we just use the selection foreground if + // set, otherwise the default bg color. + break :fg self.config.selection_foreground orelse self.background_color; + } + + // Whether we need to use the bg color as our fg color: + // - Cell is inverted and not selected + // - Cell is selected and not inverted + // Note: if selected then invert sel fg / bg must be + // false since we separately handle it if true above. + break :fg if (style.flags.inverse != selected) + bg_style orelse self.background_color + else + fg_style; + }; + + // Foreground alpha for this cell. + const alpha: u8 = if (style.flags.faint) 175 else 255; + + // If the cell has a background color, set it. + const bg_color: [4]u8 = if (bg) |rgb| bg: { + // Determine our background alpha. If we have transparency configured + // then this is dynamic depending on some situations. This is all + // in an attempt to make transparency look the best for various + // situations. See inline comments. + const bg_alpha: u8 = bg_alpha: { + const default: u8 = 255; + + if (self.config.background_opacity >= 1) break :bg_alpha default; + + // If we're selected, we do not apply background opacity + if (selected) break :bg_alpha default; + + // If we're reversed, do not apply background opacity + if (style.flags.inverse) break :bg_alpha default; + + // If we have a background and its not the default background + // then we apply background opacity + if (style.bg(cell, color_palette) != null and !rgb.eql(self.background_color)) { + break :bg_alpha default; + } + + // We apply background opacity. + var bg_alpha: f64 = @floatFromInt(default); + bg_alpha *= self.config.background_opacity; + bg_alpha = @ceil(bg_alpha); + break :bg_alpha @intFromFloat(bg_alpha); + }; + + try self.cells_bg.append(self.alloc, .{ + .mode = .bg, + .grid_col = @intCast(x), + .grid_row = @intCast(y), + .grid_width = cell.gridWidth(), + .glyph_x = 0, + .glyph_y = 0, + .glyph_width = 0, + .glyph_height = 0, + .glyph_offset_x = 0, + .glyph_offset_y = 0, + .r = rgb.r, + .g = rgb.g, + .b = rgb.b, + .a = bg_alpha, + .bg_r = 0, + .bg_g = 0, + .bg_b = 0, + .bg_a = 0, + }); + + break :bg .{ + rgb.r, rgb.g, rgb.b, bg_alpha, + }; + } else .{ + self.draw_background.r, + self.draw_background.g, + self.draw_background.b, + @intFromFloat(@max(0, @min(255, @round(self.config.background_opacity * 255)))), + }; + + // If the invisible flag is set on this cell then we + // don't need to render any foreground elements, so + // we just skip all glyphs with this x coordinate. + // + // NOTE: This behavior matches xterm. Some other terminal + // emulators, e.g. Alacritty, still render text decorations + // and only make the text itself invisible. The decision + // has been made here to match xterm's behavior for this. + if (style.flags.invisible) { + continue; + } + + // Give links a single underline, unless they already have + // an underline, in which case use a double underline to + // distinguish them. + const underline: terminal.Attribute.Underline = if (link_match_set.contains(screen, cell_pin)) + if (style.flags.underline == .single) + .double + else + .single + else + style.flags.underline; + + // We draw underlines first so that they layer underneath text. + // This improves readability when a colored underline is used + // which intersects parts of the text (descenders). + if (underline != .none) self.addUnderline( + @intCast(x), + @intCast(y), + underline, + style.underlineColor(color_palette) orelse fg, + alpha, + bg_color, + ) catch |err| { + log.warn( + "error adding underline to cell, will be invalid x={} y={}, err={}", + .{ x, y, err }, + ); + }; + + // If we're at or past the end of our shaper run then + // we need to get the next run from the run iterator. + if (shaper_cells != null and shaper_cells_i >= shaper_cells.?.len) { + shaper_run = try run_iter.next(self.alloc); + shaper_cells = null; + shaper_cells_i = 0; + } + + // If we haven't shaped this run yet, do so. + if (shaper_cells == null) if (shaper_run) |run| { + // Try to read the cells from the shaping cache if we can. + shaper_cells = self.font_shaper_cache.get(run) orelse cache: { + // Otherwise we have to shape them. + const cells = try self.font_shaper.shape(run); + + // Try to cache them. If caching fails for any reason we + // continue because it is just a performance optimization, + // not a correctness issue. + self.font_shaper_cache.put( + self.alloc, + run, + cells, + ) catch |err| { + log.warn( + "error caching font shaping results err={}", + .{err}, + ); + }; + + // The cells we get from direct shaping are always owned + // by the shaper and valid until the next shaping call so + // we can safely use them. + break :cache cells; + }; + }; + + // NOTE: An assumption is made here that a single cell will never + // be present in more than one shaper run. If that assumption is + // violated, this logic breaks. + if (shaper_cells) |cells| glyphs: { + // If there are no shaper cells for this run, ignore it. + // This can occur for runs of empty cells, and is fine. + if (cells.len == 0) break :glyphs; + + // If we encounter a shaper cell to the left of the current + // cell then we have some problems. This logic relies on x + // position monotonically increasing. + assert(cells[shaper_cells_i].x >= x); + + while (shaper_cells_i < cells.len and cells[shaper_cells_i].x == x) : ({ + shaper_cells_i += 1; + }) { + self.addGlyph( + @intCast(x), + @intCast(y), + cell_pin, + cells[shaper_cells_i], + shaper_run.?, + fg, + alpha, + bg_color, + ) catch |err| { + log.warn( + "error adding glyph to cell, will be invalid x={} y={}, err={}", + .{ x, y, err }, + ); + }; + } + } + + // Finally, draw a strikethrough if necessary. + if (style.flags.strikethrough) self.addStrikethrough( + @intCast(x), + @intCast(y), + fg, + alpha, + bg_color, + ) catch |err| { + log.warn( + "error adding strikethrough to cell, will be invalid x={} y={}, err={}", + .{ x, y, err }, + ); + }; } } @@ -1637,334 +1894,161 @@ fn addCursor( return &self.cells.items[self.cells.items.len - 1]; } -/// Update a single cell. The bool returns whether the cell was updated -/// or not. If the cell wasn't updated, a full refreshCells call is -/// needed. -fn updateCell( +/// Add an underline decoration to the specified cell +fn addUnderline( self: *OpenGL, - screen: *terminal.Screen, + x: terminal.size.CellCountInt, + y: terminal.size.CellCountInt, + style: terminal.Attribute.Underline, + color: terminal.color.RGB, + alpha: u8, + bg: [4]u8, +) !void { + const sprite: font.Sprite = switch (style) { + .none => unreachable, + .single => .underline, + .double => .underline_double, + .dotted => .underline_dotted, + .dashed => .underline_dashed, + .curly => .underline_curly, + }; + + const render = try self.font_grid.renderGlyph( + self.alloc, + font.sprite_index, + @intFromEnum(sprite), + .{ + .cell_width = 1, + .grid_metrics = self.grid_metrics, + }, + ); + + try self.cells.append(self.alloc, .{ + .mode = .fg, + .grid_col = @intCast(x), + .grid_row = @intCast(y), + .grid_width = 1, + .glyph_x = render.glyph.atlas_x, + .glyph_y = render.glyph.atlas_y, + .glyph_width = render.glyph.width, + .glyph_height = render.glyph.height, + .glyph_offset_x = render.glyph.offset_x, + .glyph_offset_y = render.glyph.offset_y, + .r = color.r, + .g = color.g, + .b = color.b, + .a = alpha, + .bg_r = bg[0], + .bg_g = bg[1], + .bg_b = bg[2], + .bg_a = bg[3], + }); +} + +/// Add a strikethrough decoration to the specified cell +fn addStrikethrough( + self: *OpenGL, + x: terminal.size.CellCountInt, + y: terminal.size.CellCountInt, + color: terminal.color.RGB, + alpha: u8, + bg: [4]u8, +) !void { + const render = try self.font_grid.renderGlyph( + self.alloc, + font.sprite_index, + @intFromEnum(font.Sprite.strikethrough), + .{ + .cell_width = 1, + .grid_metrics = self.grid_metrics, + }, + ); + + try self.cells.append(self.alloc, .{ + .mode = .fg, + .grid_col = @intCast(x), + .grid_row = @intCast(y), + .grid_width = 1, + .glyph_x = render.glyph.atlas_x, + .glyph_y = render.glyph.atlas_y, + .glyph_width = render.glyph.width, + .glyph_height = render.glyph.height, + .glyph_offset_x = render.glyph.offset_x, + .glyph_offset_y = render.glyph.offset_y, + .r = color.r, + .g = color.g, + .b = color.b, + .a = alpha, + .bg_r = bg[0], + .bg_g = bg[1], + .bg_b = bg[2], + .bg_a = bg[3], + }); +} + +// Add a glyph to the specified cell. +fn addGlyph( + self: *OpenGL, + x: terminal.size.CellCountInt, + y: terminal.size.CellCountInt, cell_pin: terminal.Pin, - cell_underline: ?terminal.Attribute.Underline, - palette: *const terminal.color.Palette, shaper_cell: font.shape.Cell, shaper_run: font.shape.TextRun, - x: usize, - y: usize, -) !bool { - const BgFg = struct { - /// Background is optional because in un-inverted mode - /// it may just be equivalent to the default background in - /// which case we do nothing to save on GPU render time. - bg: ?terminal.color.RGB, - - /// Fg is always set to some color, though we may not render - /// any fg if the cell is empty or has no attributes like - /// underline. - fg: terminal.color.RGB, - }; - - // True if this cell is selected - const selected: bool = if (screen.selection) |sel| - sel.contains(screen, cell_pin) - else - false; - + color: terminal.color.RGB, + alpha: u8, + bg: [4]u8, +) !void { const rac = cell_pin.rowAndCell(); const cell = rac.cell; - const style = cell_pin.style(cell); - const underline = cell_underline orelse style.flags.underline; - // The colors for the cell. - const colors: BgFg = colors: { - // The normal cell result - const cell_res: BgFg = if (!style.flags.inverse) .{ - // In normal mode, background and fg match the cell. We - // un-optionalize the fg by defaulting to our fg color. - .bg = style.bg(cell, palette), - .fg = style.fg(palette, self.config.bold_is_bright) orelse self.foreground_color, - } else .{ - // In inverted mode, the background MUST be set to something - // (is never null) so it is either the fg or default fg. The - // fg is either the bg or default background. - .bg = style.fg(palette, self.config.bold_is_bright) orelse self.foreground_color, - .fg = style.bg(cell, palette) orelse self.background_color, - }; + // Render + const render = try self.font_grid.renderGlyph( + self.alloc, + shaper_run.font_index, + shaper_cell.glyph_index, + .{ + .grid_metrics = self.grid_metrics, + .thicken = self.config.font_thicken, + }, + ); - // If we are selected, we our colors are just inverted fg/bg - const selection_res: ?BgFg = if (selected) .{ - .bg = if (self.config.invert_selection_fg_bg) - cell_res.fg - else - self.config.selection_background orelse self.foreground_color, - .fg = if (self.config.invert_selection_fg_bg) - cell_res.bg orelse self.background_color - else - self.config.selection_foreground orelse self.background_color, - } else null; + // If the glyph is 0 width or height, it will be invisible + // when drawn, so don't bother adding it to the buffer. + if (render.glyph.width == 0 or render.glyph.height == 0) { + return; + } - // If the cell is "invisible" then we just make fg = bg so that - // the cell is transparent but still copy-able. - const res: BgFg = selection_res orelse cell_res; - if (style.flags.invisible) { - break :colors BgFg{ - .bg = res.bg, - .fg = res.bg orelse self.background_color, - }; - } - - // If our cell has a covering glyph, then our bg is set to our fg - // so that padding extension works correctly. - if (!selected and isCovering(cell.codepoint())) { - break :colors .{ - .bg = res.fg, - .fg = res.fg, - }; - } - - break :colors res; + // If we're rendering a color font, we use the color atlas + const mode: CellProgram.CellMode = switch (try fgMode( + render.presentation, + cell_pin, + )) { + .normal => .fg, + .color => .fg_color, + .constrained => .fg_constrained, + .powerline => .fg_powerline, }; - // Alpha multiplier - const alpha: u8 = if (style.flags.faint) 175 else 255; - - // If the cell has a background, we always draw it. - const bg: [4]u8 = if (colors.bg) |rgb| bg: { - // Determine our background alpha. If we have transparency configured - // then this is dynamic depending on some situations. This is all - // in an attempt to make transparency look the best for various - // situations. See inline comments. - const bg_alpha: u8 = bg_alpha: { - const default: u8 = 255; - - if (self.config.background_opacity >= 1) break :bg_alpha default; - - // If we're selected, we do not apply background opacity - if (selected) break :bg_alpha default; - - // If we're reversed, do not apply background opacity - if (style.flags.inverse) break :bg_alpha default; - - // If we have a background and its not the default background - // then we apply background opacity - if (style.bg(cell, palette) != null and !rgb.eql(self.background_color)) { - break :bg_alpha default; - } - - // We apply background opacity. - var bg_alpha: f64 = @floatFromInt(default); - bg_alpha *= self.config.background_opacity; - bg_alpha = @ceil(bg_alpha); - break :bg_alpha @intFromFloat(bg_alpha); - }; - - try self.cells_bg.append(self.alloc, .{ - .mode = .bg, - .grid_col = @intCast(x), - .grid_row = @intCast(y), - .grid_width = cell.gridWidth(), - .glyph_x = 0, - .glyph_y = 0, - .glyph_width = 0, - .glyph_height = 0, - .glyph_offset_x = 0, - .glyph_offset_y = 0, - .r = rgb.r, - .g = rgb.g, - .b = rgb.b, - .a = bg_alpha, - .bg_r = 0, - .bg_g = 0, - .bg_b = 0, - .bg_a = 0, - }); - - break :bg .{ rgb.r, rgb.g, rgb.b, bg_alpha }; - } else .{ - self.draw_background.r, - self.draw_background.g, - self.draw_background.b, - @intFromFloat(@max(0, @min(255, @round(self.config.background_opacity * 255)))), - }; - - // If the cell has an underline, draw it before the character glyph, - // so that it layers underneath instead of overtop, since that can - // make text difficult to read. - if (underline != .none) { - const sprite: font.Sprite = switch (underline) { - .none => unreachable, - .single => .underline, - .double => .underline_double, - .dotted => .underline_dotted, - .dashed => .underline_dashed, - .curly => .underline_curly, - }; - - const render = try self.font_grid.renderGlyph( - self.alloc, - font.sprite_index, - @intFromEnum(sprite), - .{ - .cell_width = 1, - .grid_metrics = self.grid_metrics, - }, - ); - - const color = style.underlineColor(palette) orelse colors.fg; - - try self.cells.append(self.alloc, .{ - .mode = .fg, - .grid_col = @intCast(x), - .grid_row = @intCast(y), - .grid_width = 1, - .glyph_x = render.glyph.atlas_x, - .glyph_y = render.glyph.atlas_y, - .glyph_width = render.glyph.width, - .glyph_height = render.glyph.height, - .glyph_offset_x = render.glyph.offset_x, - .glyph_offset_y = render.glyph.offset_y, - .r = color.r, - .g = color.g, - .b = color.b, - .a = alpha, - .bg_r = bg[0], - .bg_g = bg[1], - .bg_b = bg[2], - .bg_a = bg[3], - }); - // If it's a wide cell we need to underline the right half as well. - if (cell.gridWidth() > 1 and x < self.grid_size.columns - 1) { - try self.cells.append(self.alloc, .{ - .mode = .fg, - .grid_col = @intCast(x + 1), - .grid_row = @intCast(y), - .grid_width = 1, - .glyph_x = render.glyph.atlas_x, - .glyph_y = render.glyph.atlas_y, - .glyph_width = render.glyph.width, - .glyph_height = render.glyph.height, - .glyph_offset_x = render.glyph.offset_x, - .glyph_offset_y = render.glyph.offset_y, - .r = color.r, - .g = color.g, - .b = color.b, - .a = alpha, - .bg_r = bg[0], - .bg_g = bg[1], - .bg_b = bg[2], - .bg_a = bg[3], - }); - } - } - - // If the shaper cell has a glyph, draw it. - if (shaper_cell.glyph_index) |glyph_index| glyph: { - // Render - const render = try self.font_grid.renderGlyph( - self.alloc, - shaper_run.font_index, - glyph_index, - .{ - .grid_metrics = self.grid_metrics, - .thicken = self.config.font_thicken, - }, - ); - - // If the glyph is 0 width or height, it will be invisible - // when drawn, so don't bother adding it to the buffer. - if (render.glyph.width == 0 or render.glyph.height == 0) { - break :glyph; - } - - // If we're rendering a color font, we use the color atlas - const mode: CellProgram.CellMode = switch (try fgMode( - render.presentation, - cell_pin, - )) { - .normal => .fg, - .color => .fg_color, - .constrained => .fg_constrained, - .powerline => .fg_powerline, - }; - - try self.cells.append(self.alloc, .{ - .mode = mode, - .grid_col = @intCast(x), - .grid_row = @intCast(y), - .grid_width = cell.gridWidth(), - .glyph_x = render.glyph.atlas_x, - .glyph_y = render.glyph.atlas_y, - .glyph_width = render.glyph.width, - .glyph_height = render.glyph.height, - .glyph_offset_x = render.glyph.offset_x + shaper_cell.x_offset, - .glyph_offset_y = render.glyph.offset_y + shaper_cell.y_offset, - .r = colors.fg.r, - .g = colors.fg.g, - .b = colors.fg.b, - .a = alpha, - .bg_r = bg[0], - .bg_g = bg[1], - .bg_b = bg[2], - .bg_a = bg[3], - }); - } - - if (style.flags.strikethrough) { - const render = try self.font_grid.renderGlyph( - self.alloc, - font.sprite_index, - @intFromEnum(font.Sprite.strikethrough), - .{ - .cell_width = 1, - .grid_metrics = self.grid_metrics, - }, - ); - - try self.cells.append(self.alloc, .{ - .mode = .fg, - .grid_col = @intCast(x), - .grid_row = @intCast(y), - .grid_width = 1, - .glyph_x = render.glyph.atlas_x, - .glyph_y = render.glyph.atlas_y, - .glyph_width = render.glyph.width, - .glyph_height = render.glyph.height, - .glyph_offset_x = render.glyph.offset_x, - .glyph_offset_y = render.glyph.offset_y, - .r = colors.fg.r, - .g = colors.fg.g, - .b = colors.fg.b, - .a = alpha, - .bg_r = bg[0], - .bg_g = bg[1], - .bg_b = bg[2], - .bg_a = bg[3], - }); - // If it's a wide cell we need to strike through the right half as well. - if (cell.gridWidth() > 1 and x < self.grid_size.columns - 1) { - try self.cells.append(self.alloc, .{ - .mode = .fg, - .grid_col = @intCast(x + 1), - .grid_row = @intCast(y), - .grid_width = 1, - .glyph_x = render.glyph.atlas_x, - .glyph_y = render.glyph.atlas_y, - .glyph_width = render.glyph.width, - .glyph_height = render.glyph.height, - .glyph_offset_x = render.glyph.offset_x, - .glyph_offset_y = render.glyph.offset_y, - .r = colors.fg.r, - .g = colors.fg.g, - .b = colors.fg.b, - .a = alpha, - .bg_r = bg[0], - .bg_g = bg[1], - .bg_b = bg[2], - .bg_a = bg[3], - }); - } - } - - return true; + try self.cells.append(self.alloc, .{ + .mode = mode, + .grid_col = @intCast(x), + .grid_row = @intCast(y), + .grid_width = cell.gridWidth(), + .glyph_x = render.glyph.atlas_x, + .glyph_y = render.glyph.atlas_y, + .glyph_width = render.glyph.width, + .glyph_height = render.glyph.height, + .glyph_offset_x = render.glyph.offset_x + shaper_cell.x_offset, + .glyph_offset_y = render.glyph.offset_y + shaper_cell.y_offset, + .r = color.r, + .g = color.g, + .b = color.b, + .a = alpha, + .bg_r = bg[0], + .bg_g = bg[1], + .bg_b = bg[2], + .bg_a = bg[3], + }); } /// Returns the grid size for a given screen size. This is safe to call