180 lines
7.4 KiB
Zig
180 lines
7.4 KiB
Zig
//! 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);
|
|
}
|