//! Drawing a pane body into a surface: its text, colours, selections and //! cursor, and joining a body with compact context rows through its layer //! (Layer.zig). const std = @import("std"); const pardes = @import("pardes.zig"); const normal = @import("normal.zig"); const edit = @import("edit.zig"); const config = @import("config.zig"); const limits = @import("memory.zig").limits; const layout = @import("layout.zig"); const modal = @import("modal.zig"); const tracy = @import("tracy.zig"); const panes = @import("panes.zig"); const colors = @import("colors.zig"); const Color = @import("surface.zig").Color; const CellStyle = @import("surface.zig").CellStyle; const Cell = @import("surface.zig").Cell; const Surface = @import("surface.zig").Surface; const pdf_enabled = panes.Pdf.enabled; const Pane = panes.Pane; const BOX_H = pardes.BOX_H; const TAG_TEXT_INSET = pardes.TAG_TEXT_INSET; const Rect = layout.Rect; const Pardes = pardes.Pardes; const tagline = @import("tagline.zig"); const Messages = @import("Messages.zig"); const Text = panes.Text; const MAX_PANES = pardes.MAX_PANES; const TagKind = @import("Layer.zig").Kind; const MAX_TAG_ROWS = pardes.MAX_TAG_ROWS; const NOTICE_LAYER_BASE = pardes.NOTICE_LAYER_BASE; const TAG_LINE_LAYER_BASE = pardes.TAG_LINE_LAYER_BASE; const HEADER_LINE_LAYER_BASE = pardes.HEADER_LINE_LAYER_BASE; const MAX_COLS = pardes.MAX_COLS; const TOPBAR_H = pardes.TOPBAR_H; const COLBAR_H = pardes.COLBAR_H; const TAG_GAP = pardes.TAG_GAP; const wordBoundsAtCol = pardes.wordBoundsAtCol; const Layer = @import("Layer.zig"); // ---- drawing a pane body into the surface ---- fn paintTerminalSelection(p: *Pardes, s: *Surface, pane: *Pane, r: Rect, rows: []const Pane.PointerRow, bg: [3]u8, fg: ?[3]u8) void { const body = bodyText(p, p.scratch.allocator(), pane) catch return; const raw = pane.body.mode == .tty; const tx = r.x + config.GUTTER; const width = r.w -| config.GUTTER; const body_y = p.bodyTop(pane, r); var lines = std.mem.splitScalar(u8, body, '\n'); var visible: i32 = 0; while (lines.next()) |line| : (visible += 1) { if (visible >= r.h -| pane.tag_rows) break; const source_row = if (raw) panes.Terminal.gridOffset(pane) + visible else pane.wrapAt(visible).line; for (rows) |row| { const target = if (row.raw_terminal) (if (raw) row.row else pane.surfRow(row.row)) else (if (raw) pane.gridRow(row.row) else row.row); if (target != source_row) continue; var lo = row.lo; var hi = row.hi; if (row.raw_terminal and !raw) { lo -|= row.prompt_bytes; hi -|= row.prompt_bytes; } else if (!row.raw_terminal and raw) { const prefix = panes.Terminal.promptPrefixBytes(pane, source_row, line); lo += prefix; hi += prefix; } lo = modal.graphemeStart(line, @min(lo, line.len)); hi = @min(hi, line.len); if (lo >= hi) continue; var col = panes.File.rawDisplayCol(line, lo); const end = if (row.to_edge and hi == line.len) width else panes.File.rawDisplayCol(line, hi); while (col < end and col < width) : (col += 1) { const cell = s.at(tx + @as(u16, @intCast(col)), body_y + @as(u16, @intCast(visible))); cell.default = false; cell.style.bg = .{ .rgb = bg }; if (fg) |ink| cell.style.fg = .{ .rgb = ink }; } } } } fn paintSourceSelection(p: *Pardes, s: *Surface, pane: *Pane, r: Rect, rows: []const Pane.PointerRow, bg: [3]u8, fg: ?[3]u8) void { if (pane.isTerminal()) return paintTerminalSelection(p, s, pane, r, rows, bg, fg); const terminal_lines = if (pane.file == null) panes.Terminal.cursorLines(p, pane) catch return else &.{}; const tx = r.x + config.GUTTER; const width = r.w -| config.GUTTER; const body_y = p.bodyTop(pane, r); const prefix: i32 = if (pane.file != null) panes.File.gutterWidth(pane) else 0; var visible: i32 = 0; while (visible < @as(i32, r.h -| pane.tag_rows)) : (visible += 1) { const source = pane.wrapAt(visible); const line = edit.pointerSourceLine(pane, terminal_lines, source.line); const start = @min(line.len, @as(usize, @intCast(@max(0, source.at)))); const next = pane.wrapAt(visible + 1); const end = if (next.line == source.line) @min(line.len, @as(usize, @intCast(@max(0, next.at)))) else line.len; for (rows) |row| { if (row.row != source.line) continue; const lo = @max(start, row.lo); const hi = @min(end, row.hi); if (lo >= hi) continue; var col = prefix + @as(i32, @intCast(panes.File.rawDisplayCol(line[start..], lo - start))); const last = if (row.to_edge and hi == end) @as(i32, width) else prefix + @as(i32, @intCast(panes.File.rawDisplayCol(line[start..], hi - start))); while (col < last and col < width) : (col += 1) { const cell = s.at(tx + @as(u16, @intCast(col)), body_y + @as(u16, @intCast(visible))); cell.default = false; cell.style.bg = .{ .rgb = bg }; if (fg) |ink| cell.style.fg = .{ .rgb = ink }; } } } } pub fn paintPointerSelection( p: *Pardes, s: *Surface, pane: *Pane, r: Rect, tx: u16, tw: u16, tag_y: u16, body_y: u16, sl: Pane.Sel, /// Rows of the selection above this are left alone. from_row: u16, bg: [3]u8, fg: ?[3]u8, ) void { if (pane.pointerSelection(sl)) |rows| return paintSourceSelection(p, s, pane, r, rows, bg, fg); var row: u16 = 0; while (row < r.h) : (row += 1) { if (row < from_row) continue; const cols = edit.sweepCols(sl, row) orelse continue; const c0: i32 = if (cols.first) cols.lo else 0; const c1: i32 = cols.hi orelse std.math.maxInt(i16); const sy = if (row < pane.tag_rows) tag_y + row else body_y + row - pane.tag_rows; // File line numbers occupy a gutter only in the body. The tag is // row zero in Sel space and starts at its real first text cell. const scroll: i32 = if (row < pane.tag_rows) pane.tag_scroll else 0; var col: i32 = if (pane.file != null and row >= pane.tag_rows) @max(c0, @as(i32, panes.File.gutterWidth(pane))) else @max(0, c0 - scroll); const row_x = if (row < pane.tag_rows) r.x + @min(TAG_TEXT_INSET, r.w) else tx; const row_width = if (row < pane.tag_rows) r.w -| TAG_TEXT_INSET else tw; while (col <= c1 - scroll and col < row_width) : (col += 1) { const cell = s.at(row_x + @as(u16, @intCast(col)), sy); cell.default = false; cell.style.bg = .{ .rgb = bg }; if (fg) |ink| cell.style.fg = .{ .rgb = ink }; } } } /// The selection colour of each pointer button's sweep: the select button's /// and the look and exec buttons', each tinted toward its own accent. pub fn buttonTints(th: *const colors.Theme) [3][3]u8 { const accent2 = if (std.mem.eql(u8, &th.num, &th.str)) th.comment else th.num; return .{ colors.mix(th.sel_bg, colors.mix(th.sel_bg, th.kw)), colors.mix(th.sel_bg, colors.mix(th.sel_bg, th.str)), colors.mix(th.sel_bg, colors.mix(th.sel_bg, accent2)), }; } /// A pane's body, painted once. A file body with compact tree-sitter /// context rows is painted into its body layer -- more rows than the grid /// has, the context rows at the tagline's height -- and the grid's rows are /// copies of that layer's first ones, at body height. Any other body has no /// rows the grid cannot show, and is painted straight onto the grid. The /// rail is the grid's alone. pub fn joinBody(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane, r: Rect, id: usize, active: bool, rail: pardes.Region) !void { const file = if (pane.file) |*f| f else return renderBody(p, s, arena, pane, r, id, active, rail, null); if (!p.settings.tree_context_tag_style or pane.collapsed or r.h <= pane.tag_rows) return renderBody(p, s, arena, pane, r, id, active, rail, null); // Which rows are context rows is decided by the text the body shows. const text = try panes.File.bodyText(arena, pane, file, p.settings.wrap); const viewport_rows = r.h - pane.tag_rows; const logical_rows = @min(@as(usize, pane.rows) + pane.context_rows, pane.wrap_line.len); if (pane.context_rows == 0 or logical_rows <= pane.context_rows) return renderBody(p, s, arena, pane, r, id, active, rail, text); pane.body_rows = @intCast(logical_rows); const bh: usize = @max(1, p.row_metrics.body_h); const th: usize = std.math.clamp(p.row_metrics.tagline_h, 1, bh); const body_pixels = @as(usize, viewport_rows) * bh; pane.context_row_limit = @intCast(@min(pane.context_rows, (body_pixels -| bh) / th)); const context_pixels = @as(usize, pane.context_row_limit.?) * th; pane.body_visible_rows = @intCast(@min(logical_rows, pane.context_row_limit.? + @max(1, (body_pixels -| context_pixels) / bh))); const width = r.w - config.GUTTER; const layer = &s.body_layers[id]; const count = logical_rows * width; if (layer.cells.len != count) layer.cells = try p.gpa.realloc(layer.cells, count); const temporary_cells = try arena.alloc(Cell, @as(usize, r.w) * (logical_rows + pane.tag_rows)); @memset(temporary_cells, .{}); var temporary: Surface = .{ .cols = r.w, .rows = @intCast(logical_rows + pane.tag_rows), .cells = temporary_cells }; const synthetic: Rect = .{ .x = 0, .y = 0, .w = r.w, .h = @intCast(logical_rows + pane.tag_rows) }; try renderBody(p, &temporary, arena, pane, synthetic, id, active, null, null); const first_row: usize = if (p.settings.tag_bottom) 0 else pane.tag_rows; for (0..logical_rows) |row| { const start = (row + first_row) * r.w + config.GUTTER; @memcpy(layer.cells[row * width ..][0..width], temporary_cells[start..][0..width]); } layer.kind = .body; layer.id = @intCast(id); layer.serial = pane.serial; const body = s.region(.body, id).?; layer.viewport = .{ .x = body.rect.x, .y = body.rect.y, .w = body.rect.w - body.clip, .h = body.rect.h }; layer.cols = width; layer.rows = @intCast(logical_rows); layer.context_rows = pane.context_rows; layer.setContextLines(pane.wrap_line[0..pane.context_rows]); layer.cursor = null; if (temporary.cursor) |cursor| { if (cursor.x >= config.GUTTER and cursor.y >= first_row) layer.cursor = .{ .x = cursor.x - config.GUTTER, .y = @intCast(cursor.y - first_row), .bar = cursor.bar }; } // The grid: the layer's first rows, as many as the body has, each a // body row high; its caret where one of them has it. (pane.rows is the // body's height after sync, and context rows are only prepared while // the rows fit the wrap table, so the layer always has these rows.) std.debug.assert(layer.rows >= viewport_rows); for (0..viewport_rows) |row| { const grid_row = s.cells[(body.rect.y + row) * s.cols ..][body.rect.x..][0..width]; @memcpy(grid_row, layer.cells[row * width ..][0..width]); } if (layer.cursor) |cursor| if (cursor.y < viewport_rows) { s.cursor = .{ .x = body.rect.x + cursor.x, .y = body.rect.y + cursor.y, .bar = cursor.bar }; }; const chrome = p.chromeTheme(); const theme = p.theme(); const pane_bg: Color = if (theme.bg) |c| .{ .rgb = c } else .default; if (rail.rect.h > 0) { s.fill(rail.rect.x, rail.rect.y, 1, rail.rect.h, .{ .bg = .{ .rgb = chrome.scroll_track } }); s.fill(rail.rect.x + 1, rail.rect.y, 1, rail.rect.h, .{ .bg = pane_bg }); s.fill(rail.rect.x, rail.rect.y + rail.thumb_y, 1, rail.thumb_h, .{ .bg = .{ .rgb = chrome.scroll_thumb } }); } } pub fn renderBody(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane, r: Rect, id: usize, active: bool, rail: ?pardes.Region, built: ?[]const u8) !void { const th = p.theme(); const chrome = p.chromeTheme(); const tx = r.x + config.GUTTER; const tw = r.w - config.GUTTER; const tag_y = p.tagTop(pane, r); // Below the notice bands: they are stacked at the top of the body the // way tree-sitter context rows are, so the body starts under them // rather than being painted over by them. const body_y = p.bodyTop(pane, r); const pane_bg: Color = if (th.bg) |c| .{ .rgb = c } else .default; const page_rgb = th.bg orelse th.tag_bg; // body: emulator rows, prompt rows hidden outside tty mode const body_h = r.h -| pane.tag_rows; const body_bottom = r.h; const body_style: CellStyle = .{ .fg = if (th.fg) |c| .{ .rgb = c } else .default, .bg = pane_bg, }; const tz_body = tracy.zone(@src(), "bodyText"); const body = built orelse try bodyText(p, arena, pane); tz_body.end(); // A select sweep begun on the tag and dragged into the body marks the // body rows it crossed only where no text is: it is laid down before // the rows, which paint over it. const sweep = pane.sel[pardes.sel_slot]; if (sweep.state != .none and (sweep.state != .done or pardes.sel_slot == 0) and @min(sweep.r0, sweep.r1) < pane.tag_rows) paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, sweep, pane.tag_rows, th.sel_bg, th.sel_fg); { // The rows themselves. `bodyText` above is only the string BUILD; // this is what writes it into the surface. const tz_rows = tracy.zone(@src(), "paneBodyRows"); defer tz_rows.end(); var it = std.mem.splitScalar(u8, body, '\n'); var i: u16 = 0; while (it.next()) |line| : (i += 1) { if (i >= body_h) break; _ = s.print(tx, body_y + i, tw, line, body_style); } } const tz_color = tracy.zone(@src(), "paneRecolor"); // File layout is independent of whether its producer requests colors. if (pane.file) |*f| { panes.File.drawGutter(p, s, pane, r, tx, tw, body_h, active); if (p.settings.colors and pane.colorAlgo() != .none) panes.File.recolorSyntax(p, s, pane, f, r, tx, tw, body_h); panes.File.drawWrapMarkers(p, s, pane, r, tx, tw, body_h, pane_bg); } else if (pane.isTerminal() and p.settings.colors) { panes.Terminal.recolorAnsi(p, s, pane, r, tx, tw, body_h, body); } tz_color.end(); const sel_btn = buttonTints(th); if (p.look_hover_preview) |preview| { if (preview.pane == id and preview.serial == pane.serial) { // Preserve body ink, then let every real user selection below // win over this quiet affordance. const preview_bg = colors.mix(page_rgb, colors.mix(page_rgb, th.sel_bg)); if (preview.file_word) |word| panes.File.paintWordSelection(p, s, pane, r, word.row, word.lo, word.hi, preview_bg) else if (preview.sel) |sel| paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, sel, pane.tag_rows, preview_bg, null); } } const modal_hover = if (p.look_hover_preview) |preview| preview.pane == id and preview.serial == pane.serial and preview.sel == null and preview.file_word == null else false; for (pane.sel, 0..) |sl, b| { if (sl.state == .none) continue; if (sl.state == .done and b != 0) continue; // A select sweep over the tag is the tag's selection, painted there // in the tag's own selection colours. if (b == pardes.sel_slot and @min(sl.r0, sl.r1) < pane.tag_rows) continue; paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, sl, pane.tag_rows, sel_btn[b], th.sel_fg); } // modal line selection (helix `x`): whole body rows, normal mode only if ((pane.body.mode == .normal or modal_hover) and pane.body.msel.active) { const hover_only = modal_hover and pane.body.mode != .normal; const modal_bg = if (hover_only) colors.mix(page_rgb, colors.mix(page_rgb, th.sel_bg)) else th.sel_bg; const lo = @min(pane.body.msel.r0, pane.body.msel.r1); const hi = @max(pane.body.msel.r0, pane.body.msel.r1); var row: u16 = pane.tag_rows; // never paint the tag row while (row < body_bottom) : (row += 1) { const ar = pane.wrapAt(@as(i32, row) - @as(i32, pane.tag_rows)).line; if (ar < lo or ar > hi) continue; var col: u16 = if (pane.file != null) panes.File.gutterWidth(pane) else 0; while (col < tw) : (col += 1) { const cell = s.at(tx + col, body_y + row - pane.tag_rows); cell.default = false; cell.style.bg = .{ .rgb = modal_bg }; if (!hover_only) cell.style.fg = .{ .rgb = th.sel_fg }; } } } const preview = normal.selRegexArmed(pane) != null; const show_prim = (pane.body.mode == .normal and pane.body.vsel.active) or preview or (modal_hover and pane.body.vsel.active); const show_extra = pane.body.mode != .tty and pane.body.nsel > 0; if (show_prim or show_extra) { const vpfx: i32 = if (pane.file != null) panes.File.gutterWidth(pane) else 0; var si: usize = 0; while (si <= pane.body.nsel) : (si += 1) { const primary = si == pane.body.nsel; if (if (primary) !show_prim else !show_extra) continue; const sr = if (primary) panes.Text.SelRange{ .row = pane.body.cur_row, .col = pane.body.cur_col, .arow = if (pane.body.vsel.active) pane.body.vsel.row else pane.body.cur_row, .acol = if (pane.body.vsel.active) pane.body.vsel.col else pane.body.cur_col, } else pane.body.sels[si]; const bnd = edit.cellBounds(sr); const hover_only = primary and modal_hover and pane.body.mode != .normal and !preview; const bg = if (hover_only) colors.mix(page_rgb, colors.mix(page_rgb, th.sel_bg)) else if (primary) th.sel_bg else colors.mix(th.sel_bg, page_rgb); var vr: i32 = 0; while (vr + @as(i32, pane.tag_rows) < @as(i32, body_bottom)) : (vr += 1) { const w = pane.wrapAt(vr); if (w.line < bnd.lo_row or w.line > bnd.hi_row) continue; const visible_line = modal.lineSlice(body, @intCast(vr)); const cstart: i32 = if (w.line == bnd.lo_row) (if (pane.file != null) panes.File.displayOffset(pane, w.line, w.at, bnd.lo_col) else panes.File.lineDisplayOffset(visible_line, @intCast(@max(0, w.at)), @intCast(@max(0, bnd.lo_col)))) + vpfx else vpfx; const cend: i32 = if (w.line == bnd.hi_row) (if (pane.file != null) panes.File.displayEndOffset(pane, w.line, w.at, bnd.hi_col) else panes.File.lineDisplayEndOffset(visible_line, @intCast(@max(0, w.at)), @intCast(@max(0, bnd.hi_col)))) + vpfx else @as(i32, tw) - 1; var col: i32 = @max(cstart, vpfx); while (col <= cend and col < tw) : (col += 1) { const cell = s.at(tx + @as(u16, @intCast(col)), body_y + @as(u16, @intCast(vr))); cell.default = false; cell.style.bg = .{ .rgb = bg }; if (!hover_only) cell.style.fg = .{ .rgb = th.sel_fg }; } } if (primary and !preview and !hover_only) continue; // the hardware cursor IS the primary's if (hover_only) continue; // quiet preview preserves the source ink const cw = pane.wrapRow(sr.row, sr.col); const crow = cw.row + @as(i32, pane.tag_rows); const ccol = (if (pane.file != null) panes.File.displayOffset(pane, sr.row, cw.at, sr.col) else panes.File.lineDisplayOffset( modal.lineSlice(body, @intCast(@max(0, cw.row))), @intCast(@max(0, cw.at)), @intCast(@max(0, sr.col)), )) + vpfx; if (crow >= pane.tag_rows and crow < @as(i32, body_bottom) and ccol >= vpfx and ccol < tw) { const cell = s.at(tx + @as(u16, @intCast(ccol)), body_y + @as(u16, @intCast(crow - pane.tag_rows))); cell.default = false; cell.style.bg = .{ .rgb = th.sel_fg }; cell.style.fg = .{ .rgb = th.sel_bg }; } } } // `gw`'s labels, two letters over the start of each word if (pane.jump.items.len > 0) labels: { const flat = edit.flatSurface(p, &pane.body) catch break :labels; const letters = config.jump_label_alphabet; const style: CellStyle = .{ .fg = .{ .rgb = th.sel_fg }, .bg = .{ .rgb = th.sel_bg }, .bold = true }; for (pane.jump.items, 0..) |word, i| { if (pane.jump_first) |first| if (i / letters.len != first) continue; var at = @min(word.anchor, word.head); for ([_]u8{ letters[i / letters.len], letters[i % letters.len] }) |letter| { const pos = modal.positionAt(flat, at); const row: i32 = @intCast(pos.row); const wp = pane.wrapRow(row, @intCast(pos.col)); const cx = if (pane.file != null) @as(i32, panes.File.gutterWidth(pane)) + panes.File.displayOffset(pane, row, wp.at, @intCast(pos.col)) else panes.File.lineDisplayOffset(modal.lineSlice(flat, pos.row), @intCast(@max(0, wp.at)), pos.col); if (wp.row >= 0 and wp.row < body_h and cx >= 0 and cx < tw) s.set(tx + @as(u16, @intCast(cx)), body_y + @as(u16, @intCast(wp.row)), &.{letter}, style); at = modal.nextGrapheme(flat, at); } } } // cursor: tracks the shell cursor until pinned by a click or a key // (the tag's or a prompt's cursor wins while that is being typed into) if (active and pane.focus == .body and (pane.prompt == .none or pane.prompt == .del_side or pane.prompt == .repl_choice)) { if (pane.body.mode != .tty) { const cur = panes.Terminal.gridCursor(pane); const goff = panes.Terminal.gridOffset(pane); const crow = if (pane.body.cur_pinned) pane.body.cur_row else pane.surfRow(@as(i32, @intCast(cur.y)) + goff); const ccol = if (pane.body.cur_pinned) pane.body.cur_col else @as(i32, @intCast(cur.x)); const cwp = pane.wrapRow(crow, ccol); const prow = cwp.row + @as(i32, pane.tag_rows); // Files store source-byte columns; the Surface stores display // cells, so account for every expanded tab before the cursor. const cx = if (pane.file != null) @as(i32, panes.File.gutterWidth(pane)) + panes.File.displayOffset(pane, crow, cwp.at, ccol) else if (pane.body.cur_pinned) panes.File.lineDisplayOffset( modal.lineSlice(body, @intCast(@max(0, cwp.row))), @intCast(@max(0, cwp.at)), @intCast(@max(0, ccol)), ) else ccol; if (prow >= pane.tag_rows and cx >= 0 and prow < body_bottom and cx < tw) s.cursor = .{ .x = tx + @as(u16, @intCast(cx)), .y = body_y + @as(u16, @intCast(prow - pane.tag_rows)), .bar = pane.body.mode == .insert }; } else if (panes.Terminal.visibleCursor(pane)) |cur| { if (cur.y + pane.tag_rows < body_bottom and cur.x < tw) s.cursor = .{ .x = tx + cur.x, .y = body_y + cur.y }; } } if (rail) |bar| if (bar.rect.h > 0) { // The rail covers the notice bands too: nothing else paints those // two gutter columns, so a gap there keeps the frame-wide border // fill and reads as a notch cut out of the scrollbar. s.fill(bar.rect.x, bar.rect.y, 1, bar.rect.h, .{ .bg = .{ .rgb = chrome.scroll_track } }); s.fill(bar.rect.x + 1, bar.rect.y, 1, bar.rect.h, .{ .bg = pane_bg }); s.fill(bar.rect.x, bar.rect.y + bar.thumb_y, 1, bar.thumb_h, .{ .bg = .{ .rgb = chrome.scroll_thumb } }); }; } pub fn bodyText(p: *Pardes, arena: std.mem.Allocator, pane: *Pane) ![]const u8 { if (pane.image != null) { const buf = try arena.alloc(u8, pane.rows -| 1); @memset(buf, '\n'); return buf; } if (pane.hasPdf()) { if (comptime pdf_enabled) return panes.Pdf.visibleText( &pane.pdf.?, p.pdf_gpa, arena, pane.rows -| BOX_H, ); unreachable; } if (pane.file) |*f| return panes.File.bodyText(arena, pane, f, p.settings.wrap); return panes.Terminal.bodyText(arena, pane); }