Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,11 @@ the whole script.
The map fits the mission without its scenery, which lies far off. Each frame it gathers the things
that would overlap on the screen into one mark, the most important first: the selection, the player,
the ships, the markers, the camera's points and the scenery. Names then go in the same order, each
on the first side of its mark where it covers no mark and no name, and crosses no route where a side
is free. The map draws its own grid, so that its scale is always a cell, and draws only the part of
a range's ring or a route's leg that shows. A dragged ship or marker is queued as an edit of its
place, as the forms' edits are, when it's let go.
on the first side of its mark where it covers no mark, no name and none of the notes over the map,
such as the legend and the scale. A side where the name also crosses no route comes first. The map
draws its own grid, so that its scale is always a cell, and draws only the part of a range's ring or
a route's leg that shows. A dragged ship or marker is queued as an edit of its place, as the forms'
edits are, when it's let go.

## The debugger

Expand Down
3 changes: 2 additions & 1 deletion docs/debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ inspector names its thread.
| Pause | F6 | F8 | Pauses the whole mission, ships and all, or lets it go on |

- **Threads** lists the script's threads: where each one is, what started it, its call depth and
what it waits for. A double click goes to a thread's line.
what it waits for. Where shows a thread's line, and its offset too where the column has room. A
double click goes to a thread's line.
- **Variables** shows the game's variables that the source uses and the mission's globals, with
their values in the game. A value that changed since the last hold is orange. While the script
runs, the editor polls the game for them, 4 times a second by default (Settings · Editor link ·
Expand Down
5 changes: 3 additions & 2 deletions docs/editing.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ holds, named after its most important thing.
click on an empty part of the map, shows the whole mission again.
- Point at a numbered mark to list what it holds, and click it to zoom in to them. Where its
things lie on the same spot, the click lists them instead, to pick one.
- Names go where they have room, the selection's and the player's first, and keep off the routes
where they can. Point at a mark to see a name that has no room.
- Names go where they have room, the selection's and the player's first. They never cover the
legend, the scale or the other notes on the map, and keep off the routes where they can. Point at
a mark to see a name that has no room.
- Click a ship or a marker to select it: the inspector shows it, and the map marks it with its
place. Drag it to move where it starts: its `position` changes in the source when you let go, as
one step of Undo. Hold Shift to snap to 100. A click on an empty part of the map clears the
Expand Down
3 changes: 2 additions & 1 deletion src/palette.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const App = app_mod.App;
const commands = @import("commands.zig");
const Command = commands.Command;
const game = @import("game.zig");
const panels = @import("panels.zig");
const ui = @import("ui.zig");
const fonts = ui.fonts;
const gui = ui.gui;
Expand Down Expand Up @@ -282,7 +283,7 @@ fn playForm(app: *App) void {
const watched = if (state.watch) |index| if (index < found_ships.len) found_ships[index] else null else null;
if (widgets.frame("watch", .{
.label = if (watched) |ship| ship.title else "The player's ship",
.mark = if (watched) |ship| .{ .icon = .{ .icon = .navigation_2, .color = if (ship.group) |group| theme.groups[group % theme.groups.len] else theme.text_menu } } else .none,
.mark = if (watched) |ship| .{ .icon = .{ .icon = .navigation_2, .color = panels.groupColor(ship.*) } } else .none,
})) _ = c.ImGui_OpenPopup("ships", 0);
if (c.ImGui_BeginPopup("ships", 0)) {
if (c.ImGui_MenuItemEx("The player's ship", null, state.watch == null, true)) state.watch = null;
Expand Down
84 changes: 54 additions & 30 deletions src/panels.zig
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ const App = app_mod.App;
const Panel = app_mod.Panel;
const commands = @import("commands.zig");
const game = @import("game.zig");
const Document = @import("mission/document.zig").Document;
const document_mod = @import("mission/document.zig");
const Document = document_mod.Document;
const forms = @import("forms.zig");
const scene_view = @import("views/scene.zig");
const script_view = @import("views/script.zig");
Expand Down Expand Up @@ -139,7 +140,7 @@ fn tabCount(app: *App, panel: Panel) ?widgets.Pill {
break :blk .{ .text = gui.print("{d}", .{found}), .style = if (found > 0) .err else .normal };
},
.breakpoints => blk: {
var buffer: [@import("mission/document.zig").max_breakpoints]usize = undefined;
var buffer: [document_mod.max_breakpoints]usize = undefined;
break :blk .{ .text = gui.print("{d}", .{document.breakpoints(&buffer).len}) };
},
else => null,
Expand Down Expand Up @@ -175,10 +176,13 @@ fn padded(x: f32, y: f32) void {
gui.setCursor(gui.add(gui.cursor(), .{ .x = px(x), .y = px(y) }));
}

/// A line that says the panel needs something the editor doesn't have yet.
/// A line that says the panel needs something the editor doesn't have yet, wrapped at the
/// panel's edge.
fn waiting(icon: Icon, text: []const u8, plan: ?commands.Plan) void {
padded(12, 12);
const at = gui.cursor();
gui.wrapAt(at.x + gui.available().x - px(12));
defer c.ImGui_PopTextWrapPos();
widgets.iconItem(icon, fonts.size.icon_small, theme.text_off);
gui.setCursor(.{ .x = at.x + px(22), .y = at.y - px(1) });
widgets.caption(text, theme.text_dim, fonts.size.small, .regular);
Expand Down Expand Up @@ -278,7 +282,7 @@ fn outline(app: *App) void {

/// How the outline shows `record`.
fn recordRow(document: *const Document, record: game.Record, selected: bool, failed: bool) widgets.Row {
const group_color = if (record.group) |group| theme.groups[group % theme.groups.len] else theme.text_menu;
const group_color = groupColor(record);
var shown: widgets.Row = .{ .label = record.title, .indent = 52, .selected = selected, .failed = failed };
shown.mark = switch (record.section) {
.flight_groups => .{ .swatch = group_color },
Expand Down Expand Up @@ -324,6 +328,9 @@ const Find = struct {
var query: [128:0]u8 = @splat(0);
};

/// The most lines Find lists.
const most_found = 500;

fn find(app: *App) void {
const document = app.document orelse return waiting(.search, "No mission open", null);
padded(8, 8);
Expand All @@ -342,14 +349,13 @@ fn find(app: *App) void {
found += 1;
gui.pushIdInt(line);
defer gui.popId();
const shown = std.mem.trim(u8, text, " \t");
const result = widgets.row("line", .{
.label = gui.z(shown[0..@min(shown.len, 60)]),
.label = std.mem.trim(u8, text, " \t"),
.mono_label = true,
.end = .{ .text = .{ .text = gui.print("{d}", .{line + 1}) } },
});
if (result.clicked) app.goTo(@intCast(line), @intCast(column));
if (found >= 500) break;
if (found >= most_found) break;
}
if (found == 0) {
padded(10, 6);
Expand All @@ -359,6 +365,11 @@ fn find(app: *App) void {

// The debugging panels, which need the game linked.

/// Where a table's first column starts, and the room a cell's text leaves before what follows it,
/// in design pixels: text that would come closer is cut with an ellipsis.
const table_inset = 12;
const cell_gap = 8;

fn tableHeader(columns: []const []const u8, widths: []const f32) void {
const at = gui.cursor();
const width = gui.available().x;
Expand All @@ -367,7 +378,7 @@ fn tableHeader(columns: []const []const u8, widths: []const f32) void {
gui.rect(list, at, .{ .x = at.x + width, .y = at.y + height }, theme.table_head, 0);
gui.rect(list, .{ .x = at.x, .y = at.y + height - px(1) }, .{ .x = at.x + width, .y = at.y + height }, theme.table_head_line, 0);
gui.sans(.medium, fonts.size.ui);
var x = at.x + px(12);
var x = at.x + px(table_inset);
for (columns, widths) |column, column_width| {
gui.drawText(list, .{ .x = x, .y = gui.textTop(at.y + height / 2) }, theme.text_dim, column);
x += px(column_width);
Expand Down Expand Up @@ -412,7 +423,7 @@ fn threadRow(app: *App, state: State, thread: State.Thread) void {
if (held) gui.rect(list, at, .{ .x = at.x + width, .y = at.y + height }, theme.held_row, 0) else if (gui.hovered()) gui.rect(list, at, .{ .x = at.x + width, .y = at.y + height }, theme.row_hover, 0);
gui.rect(list, .{ .x = at.x, .y = at.y + height - px(1) }, .{ .x = at.x + width, .y = at.y + height }, theme.table_line, 0);
const middle = at.y + height / 2;
var x = at.x + px(12);
var x = at.x + px(table_inset);
gui.mono(.regular, fonts.size.ui);
gui.drawText(list, .{ .x = x, .y = gui.textTop(middle) }, theme.text_row, gui.print("{d}", .{thread.slot}));
gui.popFont();
Expand All @@ -426,37 +437,40 @@ fn threadRow(app: *App, state: State, thread: State.Thread) void {
gui.popFont();
}
x += px(thread_widths[1]);
// Where: its line, from the build's map, and its offset.
// Where: its line, from the build's map, and its offset after it where the column has room.
const where_limit = x + px(thread_widths[2] - cell_gap);
const line = if (thread.at) |offset| session.lineOf(offset) else null;
var where_x = x;
if (line) |found| {
gui.mono(.regular, fonts.size.ui);
const text = gui.print("line {d}", .{found + 1});
gui.drawText(list, .{ .x = where_x, .y = gui.textTop(middle) }, if (held) theme.held_text else theme.text_row, text);
widgets.clippedText(list, .{ .x = where_x, .y = gui.textTop(middle) }, where_limit, if (held) theme.held_text else theme.text_row, text);
where_x += gui.textSize(text).x;
gui.popFont();
}
gui.sans(.regular, fonts.size.ui);
const offset_text = if (thread.at) |offset| gui.print("{s}offset {d}", .{ if (line != null) " · " else "", offset }) else "outside the script";
gui.drawText(list, .{ .x = where_x, .y = gui.textTop(middle) }, theme.text_dim, offset_text);
if (line == null or where_x + gui.textSize(offset_text).x <= where_limit) {
widgets.clippedText(list, .{ .x = where_x, .y = gui.textTop(middle) }, where_limit, theme.text_dim, offset_text);
}
gui.popFont();
x += px(thread_widths[2]);
gui.mono(.regular, fonts.size.ui);
gui.drawText(list, .{ .x = x, .y = gui.textTop(middle) }, theme.text_row, startedBy(app, thread, line));
widgets.clippedText(list, .{ .x = x, .y = gui.textTop(middle) }, x + px(thread_widths[3] - cell_gap), theme.text_row, startedBy(app, thread, line));
x += px(thread_widths[3]);
gui.drawText(list, .{ .x = x, .y = gui.textTop(middle) }, theme.text_row, gui.print("{d}", .{thread.depth}));
gui.popFont();
x += px(thread_widths[4]);
gui.sans(.regular, fonts.size.ui);
const waits_for = if (thread.interrupted) "its trigger" else if (thread.wake != 0) gui.print("clock {d} s", .{thread.wake}) else "nothing";
gui.drawText(list, .{ .x = x, .y = gui.textTop(middle) }, theme.text_dim, waits_for);
widgets.clippedText(list, .{ .x = x, .y = gui.textTop(middle) }, at.x + width - px(cell_gap), theme.text_dim, waits_for);
gui.popFont();
if (double_clicked) if (line) |found| app.goTo(found, 0);
}

/// The design's tag of the thread the game holds: the state on the accent, 7 pixels to each side.
fn stateTag(list: *c.ImDrawList, left: f32, middle: f32, text: []const u8) void {
gui.sans(.semibold, 11);
gui.sans(.semibold, fonts.size.note);
defer gui.popFont();
const height = px(17);
const max = gui.vec2(left + gui.textSize(text).x + px(7) * 2, middle + height / 2);
Expand All @@ -469,24 +483,30 @@ fn startedBy(app: *App, thread: State.Thread, line: ?u32) []const u8 {
const document = app.document orelse return "";
const known = document.outline();
if (thread.trigger) |index| {
for (known.records) |record| {
if (record.section == .triggers and record.index == index) return gui.print("on({s}, \"{s}\")", .{ record.subject, record.condition });
}
if (known.nth(.triggers, index)) |record| return triggerName(record.*);
return gui.print("trigger {d}", .{index});
}
if (thread.depth == 0) if (line) |found| if (known.at(found)) |record| if (record.section == .parts) return record.title;
return "a part";
}

/// A trigger as the source writes it: `on(subject, "condition")`.
fn triggerName(record: game.Record) [:0]const u8 {
return gui.print("on({s}, \"{s}\")", .{ record.subject, record.condition });
}

const breakpoint_columns = [_][]const u8{ "Line", "Statement", "In" };
const breakpoint_widths = [_]f32{ 64, 330, 160 };

fn breakpoints(app: *App) void {
const document = app.document orelse return waiting(.circle_dot, "No mission open", null);
var buffer: [@import("mission/document.zig").max_breakpoints]usize = undefined;
var buffer: [document_mod.max_breakpoints]usize = undefined;
const lines = document.breakpoints(&buffer);
if (lines.len == 0) {
waiting(.circle_dot, gui.print("No breakpoints · {s} or a click in the margin sets one", .{app.keyText(.toggle_breakpoint) orelse "F9"}), null);
return;
}
tableHeader(&.{ "Line", "Statement", "In" }, &.{ 64, 330, 160 });
tableHeader(&breakpoint_columns, &breakpoint_widths);
const known = document.outline();
for (lines) |line| {
gui.pushIdInt(line);
Expand All @@ -504,11 +524,13 @@ fn breakpoints(app: *App) void {
if (app.session.mapped()) gui.dot(list, .{ .x = at.x + px(17), .y = middle }, 4.5, theme.breakpoint) else gui.ring(list, .{ .x = at.x + px(17), .y = middle }, 3.75, 1.5, theme.breakpoint);
gui.mono(.regular, fonts.size.ui);
gui.drawText(list, .{ .x = at.x + px(28), .y = gui.textTop(middle) }, theme.text_row, gui.print("{d}", .{line + 1}));
gui.drawText(list, .{ .x = at.x + px(76), .y = gui.textTop(middle) }, theme.syntax.name, statement[0..@min(statement.len, 48)]);
const statement_x = at.x + px(table_inset + breakpoint_widths[0]);
const record_x = statement_x + px(breakpoint_widths[1]);
widgets.clippedText(list, .{ .x = statement_x, .y = gui.textTop(middle) }, record_x - px(cell_gap), theme.syntax.name, statement);
gui.popFont();
if (record) |found| {
gui.mono(.regular, fonts.size.ui);
gui.drawText(list, .{ .x = at.x + px(406), .y = gui.textTop(middle) }, theme.text_dim, found.title);
widgets.clippedText(list, .{ .x = record_x, .y = gui.textTop(middle) }, at.x + width - px(cell_gap), theme.text_dim, found.title);
gui.popFont();
}
if (clicked) app.goTo(@intCast(line), 0);
Expand Down Expand Up @@ -713,8 +735,8 @@ fn usedIn(document: ?*Document, under: []const u8, name: []const u8) bool {
return false;
}

/// A variable's line, as the design's `.vr`: its name in the code font, its value at the end, in
/// orange where it changed since the last hold.
/// A variable's line, as the design's `.vr`: its name in the code font, cut short of its value,
/// and its value at the end, in orange where it changed since the last hold.
fn variableRow(name: []const u8, value: []const u8, color: theme.Color, changed: bool) void {
const at = gui.cursor();
const width = gui.available().x;
Expand All @@ -723,8 +745,9 @@ fn variableRow(name: []const u8, value: []const u8, color: theme.Color, changed:
const list = gui.drawList();
gui.mono(.regular, fonts.size.ui);
const y = gui.textTop(at.y + height / 2);
gui.drawText(list, .{ .x = at.x + px(34), .y = y }, color, name);
gui.drawText(list, .{ .x = at.x + width - px(12) - gui.textSize(value).x, .y = y }, if (changed) theme.accent else theme.text_row, value);
const value_x = at.x + width - px(12) - gui.textSize(value).x;
widgets.clippedText(list, .{ .x = at.x + px(34), .y = y }, value_x - px(cell_gap), color, name);
gui.drawText(list, .{ .x = value_x, .y = y }, if (changed) theme.accent else theme.text_row, value);
gui.popFont();
}

Expand Down Expand Up @@ -760,7 +783,7 @@ fn inspector(app: *App) void {
gui.setCursor(.{ .x = at.x + px(44), .y = at.y });
c.ImGui_BeginGroup();
const heading = switch (record.section) {
.triggers => gui.print("on({s}, \"{s}\")", .{ record.subject, record.condition }),
.triggers => triggerName(record.*),
else => gui.z(record.title),
};
widgets.monoCaption(heading, theme.text_strong, 13.5, .semibold);
Expand Down Expand Up @@ -835,8 +858,9 @@ pub fn capitalNoun(section: game.Section) []const u8 {
};
}

/// The colour of `record`'s flight group, for its icon in the outline and its mark on the map.
pub fn groupColor(record: game.Record) theme.Color {
return if (record.group) |group| theme.groups[group % theme.groups.len] else theme.text_menu;
return if (record.group) |group| theme.group(group) else theme.text_menu;
}

/// The first lines of a record's code, in a small code view with their numbers.
Expand Down Expand Up @@ -869,8 +893,8 @@ fn codeExcerpt(document: *Document, record: game.Record, width: f32) void {
// The reference: what the word at the cursor is, from the game's catalogues.

fn reference(app: *App) void {
const word_name = app.reference_word orelse return waiting(.book_open, "Put the cursor on a command or a word of mission source", null);
const word = app.language.find(word_name) orelse return waiting(.book_open, "Put the cursor on a command or a word of mission source", null);
const word = (if (app.reference_word) |name| app.language.find(name) else null) orelse
return waiting(.book_open, "Put the cursor on a command or a word of mission source", null);
gui.pushVar2(c.ImGuiStyleVar_WindowPadding, .{ .x = px(12), .y = px(12) });
_ = c.ImGui_BeginChild("reference", .{ .x = 0, .y = 0 }, c.ImGuiChildFlags_AlwaysUseWindowPadding, 0);
gui.popVar(1);
Expand Down
2 changes: 1 addition & 1 deletion src/screens/settings.zig
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ fn portField(app: *App) void {

/// The design's `.plan`: what's planned, in a small grey pill, its middle at the cursor.
fn planTag(text: []const u8) void {
gui.sans(.semibold, 11);
gui.sans(.semibold, fonts.size.note);
defer gui.popFont();
const at = gui.cursor();
const width = gui.textSize(text).x + px(7) * 2;
Expand Down
2 changes: 1 addition & 1 deletion src/screens/start.zig
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ fn yourMods(app: *App, width: f32) void {

/// The design's `.tag`: a small rounded label, right-aligned at `right`.
fn tag(list: *c.ImDrawList, right_middle: gui.Vec2, text: []const u8, background: theme.Color, foreground: theme.Color) void {
gui.sans(.semibold, 11);
gui.sans(.semibold, fonts.size.note);
defer gui.popFont();
const width = gui.spacedWidth(text, 0.33) + px(7) * 2;
const height = px(20);
Expand Down
2 changes: 2 additions & 0 deletions src/ui/fonts.zig
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ pub const size = struct {
pub const small: f32 = 12;
/// The status bar, counts and file paths.
pub const smaller: f32 = 11.5;
/// Tags, such as Held, and the notes over the map.
pub const note: f32 = 11;
/// Key chips, pills, badges and section headings.
pub const tiny: f32 = 10.5;
/// Icons in buttons, and the larger icons beside text.
Expand Down
11 changes: 11 additions & 0 deletions src/ui/theme.zig
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ pub const groups = [_]Color{
rgb(0x5aa9ff), rgb(0xc9cfdb), rgb(0xff7a6b), rgb(0x8fd18a), rgb(0xc4a7ff), rgb(0xe7c06b), rgb(0x63d1c0), rgb(0xf4b27a),
};

/// The colour of the flight group at `index` (`groups`).
pub fn group(index: usize) Color {
return groups[index % groups.len];
}

/// The design's panels: 3 pixels apart and 3 pixels inside the area they fill, each in a line of 1
/// pixel with corners of 3. The editor draws the line around each dock node, so the nodes are the
/// panels' insides.
Expand Down Expand Up @@ -292,3 +297,9 @@ test lighten {
try std.testing.expectEqual(rgb(0x5aa9ff), lighten(rgb(0x5aa9ff), 0));
try std.testing.expectEqual(rgb(0xffffff), lighten(rgb(0x5aa9ff), 1));
}

test group {
// The player's wing is the first group, and the colours start again after the last.
try std.testing.expectEqual(rgb(0x5aa9ff), group(0));
try std.testing.expectEqual(group(1), group(groups.len + 1));
}
Loading
Loading