//! Unit conversion mode drawing for the TUI. //! //! Layout: a wrapping row of category chips, the live conversion result, the //! conversion factor, and two side-by-side unit columns (from / to). Every //! chip and unit name is registered as a clickable region, so the whole view is //! usable with the mouse as well as the keyboard. const std = @import("std"); const vaxis = @import("vaxis"); const vxfw = vaxis.vxfw; const engine = @import("engine"); const draw = @import("draw.zig"); const tui = @import("../tui.zig"); const C = draw.C; const units = engine.units; /// Column layout constants. const from_col: u16 = 4; const to_col_offset: u16 = 26; pub fn drawConvertMode(app: *tui.App, surface: *vxfw.Surface, width: u16, height: u16) void { const table = units.unitsIn(app.conv_category); const pair = app.convUnits(); const zone_active = app.value_zone_active; // -- Category chips (wrapping) -- draw.writeStr(surface, 2, 2, "Category:", .{ .fg = C.muted }); var row: u16 = 3; var col: u16 = 4; for (std.enums.values(engine.UnitCategory)) |category| { const label = category.label(); const chip_len: u16 = @intCast(label.len + 2); // Wrap when the chip would run off the right edge. if (col + chip_len >= width -| 2) { row += 1; col = 4; } // And stop before the furniture rows, so a narrow terminal does not draw // chips over the separator and the prompt. if (row >= height -| 3) break; const selected = category == app.conv_category; const focused = selected and zone_active and app.conv_zone == .category; const style: vaxis.Style = if (focused) .{ .fg = C.bg, .bg = C.cyan, .bold = true } else if (selected) .{ .fg = C.bg, .bg = C.purple, .bold = true } else .{ .fg = C.muted }; draw.writeChar(surface, row, col, ' ', style); draw.writeStr(surface, row, col + 1, label, style); draw.writeChar(surface, row, col + 1 + @as(u16, @intCast(label.len)), ' ', style); app.addRegion(row, col, chip_len, .{ .conv_category = category }); col += chip_len + 1; } // -- Result -- row += 2; // Exact conversion, so a terminating case like 12 in -> ft shows 1 rather // than 0.9999999999999998. var converted = units.convertExactUnits(app.allocator, app.conv_value, pair.from, pair.to) catch app.conv_value.clone() catch return; defer converted.deinit(); const shown_in = app.conv_value.render(app.allocator, tui.display_format) catch return; defer shown_in.deinit(app.allocator); const shown_out = converted.render(app.allocator, tui.display_format) catch return; defer shown_out.deinit(app.allocator); var line_buf: [192]u8 = undefined; const input_line = std.fmt.bufPrint(&line_buf, "{s} {s}", .{ shown_in.text, pair.from.name }) catch "?"; draw.writeStr(surface, row, 2, input_line, .{ .fg = C.fg }); row += 1; var result_buf: [192]u8 = undefined; const result_line = std.fmt.bufPrint(&result_buf, "= {s} {s}", .{ shown_out.text, pair.to.name }) catch "?"; draw.writeStr(surface, row, 2, result_line, .{ .fg = C.green, .bold = true }); row += 1; // -- Conversion factor (linear conversions only) -- if (pair.from.isLinear() and pair.to.isLinear()) { const factor = pair.from.to_base_factor / pair.to.to_base_factor; const factor_shown = engine.Number.fromFloat(factor).render(app.allocator, tui.compact_format) catch return; defer factor_shown.deinit(app.allocator); var factor_buf: [160]u8 = undefined; const factor_line = std.fmt.bufPrint(&factor_buf, "1 {s} = {s} {s}", .{ pair.from.name, factor_shown.text, pair.to.name, }) catch "?"; draw.writeStr(surface, row, 2, factor_line, .{ .fg = C.muted }); } else { // Temperature is affine, so no single factor describes it. draw.writeStr(surface, row, 2, "affine conversion (offset), no single factor", .{ .fg = C.muted }); } row += 2; // -- Unit columns -- const to_col = from_col + to_col_offset; const from_focused = zone_active and app.conv_zone == .from; const to_focused = zone_active and app.conv_zone == .to; draw.writeStr(surface, row, 2, "From", if (from_focused) .{ .fg = C.cyan, .bold = true } else .{ .fg = C.muted }); draw.writeStr(surface, row, to_col -| 2, "To", if (to_focused) .{ .fg = C.cyan, .bold = true } else .{ .fg = C.muted }); row += 1; const list_start = row; // One row is reserved for the overflow notice, so a hidden unit gets reported // rather than the notice being written over the last unit drawn. It used to // land on `list_end - 1`, erasing the unit already there and undercounting the // remainder by one. const list_end = height -| 4; const room: usize = if (list_end > list_start) list_end - list_start else 0; const overflows = table.len > room; const visible: usize = if (overflows) (if (room > 0) room - 1 else 0) else room; for (table, 0..) |unit, i| { if (i >= visible) break; const list_row: u16 = list_start + @as(u16, @intCast(i)); drawUnitCell(app, surface, list_row, from_col, unit, i == app.conv_from_idx, from_focused, .{ .conv_from = i }); drawUnitCell(app, surface, list_row, to_col, unit, i == app.conv_to_idx, to_focused, .{ .conv_to = i }); } // If the terminal is too short for the whole list, say so rather than silently // truncating. Drawn even when nothing fits at all, which is the case that used // to show two empty columns and no explanation. if (overflows) { var more_buf: [64]u8 = undefined; const more = std.fmt.bufPrint(&more_buf, "... {d} more (resize to see all)", .{table.len - visible}) catch "..."; const notice_row = list_start + @as(u16, @intCast(visible)); if (notice_row < list_end) { draw.writeStr(surface, notice_row, from_col, more, .{ .fg = C.dim }); } } // -- Separator, input, status -- draw.fillRow(surface, height -| 3, '-', .{ .fg = C.dim }); app.drawInput(surface, height -| 2); app.addRegion(height -| 2, 0, width, .focus_input); draw.fillRow(surface, height -| 1, ' ', .{ .fg = C.muted, .bg = C.bg }); const status = if (zone_active) "Tab:mode | ?:help | `:input | Arrows:select | Ctrl-S:swap" else "?:help | Tab:mode | Type a value + Enter | `:select units | Ctrl-S:swap"; draw.hintRow(surface, height -| 1, 1, status, .{ .fg = C.muted, .bg = C.bg }); } /// Draw one unit name in a column, highlighting it when selected, and register /// it as clickable. fn drawUnitCell( app: *tui.App, surface: *vxfw.Surface, row: u16, col: u16, unit: engine.UnitDef, selected: bool, column_focused: bool, action: tui.Action, ) void { const style: vaxis.Style = if (selected and column_focused) .{ .fg = C.bg, .bg = C.cyan, .bold = true } else if (selected) .{ .fg = C.green, .bold = true } else .{ .fg = C.fg }; const marker: u8 = if (selected) '>' else ' '; draw.writeChar(surface, row, col -| 2, marker, .{ .fg = C.cyan, .bold = true }); draw.writeStr(surface, row, col, unit.name, style); // Make the whole column slot clickable, not just the name text, so short // names like "m" are not a one-character target. app.addRegion(row, col -| 2, to_col_offset -| 2, action); }