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
2 changes: 2 additions & 0 deletions docs/editing.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ holds, named after its most important thing.
- **Routes** shows where the script sends each ship and flight group (see below).
- **Follow** keeps a ship in the middle of the map.
- Where the map doesn't show the player's start, a note at its edge points to it.
- In a narrow window, the toolbar's chips show only their icons: point at one for its name. Where
that's not enough, Follow's label goes, then Live and Orders, and then Follow.

**Live** and **Orders** show where the ships are and what they do in the running game, and come
with the live scene ([#21](https://github.com/OpenReliant/openreliant-editor/issues/21)).
Expand Down
56 changes: 43 additions & 13 deletions src/ui/widgets.zig
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,8 @@ pub const Frame = struct {
disabled: ?Disabled = null,
/// Its width; by default the space left on the line.
width: ?f32 = null,
/// What it's for, where no label beside it says so.
tooltip: ?[]const u8 = null,
};

/// The design's `.frame`: a field that opens a list, as the inspector's.
Expand All @@ -794,7 +796,7 @@ pub fn frame(id: []const u8, shown: Frame) bool {
gui.popFont();
};
if (shown.chevron) drawIcon(list, .{ .x = max.x - px(8) - px(12), .y = middle }, .chevron_down, fonts.size.icon_tiny, theme.text_dim);
if (shown.disabled) |off| gui.tooltip(off.reason);
if (shown.disabled) |off| gui.tooltip(off.reason) else if (shown.tooltip) |tip| gui.tooltip(tip);
return clicked;
}

Expand Down Expand Up @@ -1033,26 +1035,54 @@ pub fn choice(id: []const u8, options: []const []const u8, current: usize, look:
}

/// The design's `.chip` that turns something on and off, such as the map's Ranges: an icon and a
/// label, lit while on. True when clicked.
pub fn toggleChip(id: []const u8, label: []const u8, icon: Icon, on: bool, disabled: ?Disabled) bool {
gui.sans(.regular, fonts.size.small);
defer gui.popFont();
/// label, lit while on.
pub const ToggleChip = struct {
label: []const u8,
icon: Icon,
on: bool = false,
disabled: ?Disabled = null,
/// The icon alone, with the label in its tooltip, for a bar without room for the label.
icon_only: bool = false,

/// In design pixels: the padding at each end, the gap between the icon and the label, and the
/// width of a chip with its icon alone.
const padding = 9;
const gap = 6;
const icon_size = fonts.size.icon_small;
const width = px(9) + px(icon_size) + px(6) + gui.textSize(label).x + px(9);
pub const icon_only_width = padding * 2 + icon_size;

pub fn width(shown: ToggleChip) f32 {
if (shown.icon_only) return px(icon_only_width);
gui.sans(.regular, fonts.size.small);
defer gui.popFont();
return px(padding * 2 + icon_size + gap) + gui.textSize(shown.label).x;
}
};

/// Draws `shown` at the cursor. True when clicked.
pub fn toggleChip(id: []const u8, shown: ToggleChip) bool {
const width = shown.width();
const height = px(24);
const at = gui.cursor();
const clicked = c.ImGui_InvisibleButton(gui.z(id), .{ .x = width, .y = height }, 0) and disabled == null;
if (disabled == null) gui.handCursor();
const clicked = c.ImGui_InvisibleButton(gui.z(id), .{ .x = width, .y = height }, 0) and shown.disabled == null;
if (shown.disabled == null) gui.handCursor();
const list = gui.drawList();
const max = gui.vec2(at.x + width, at.y + height);
const lit = on and disabled == null;
const lit = shown.on and shown.disabled == null;
gui.rect(list, at, max, if (lit) theme.pill else theme.field, 4);
gui.outline(list, at, max, if (lit) theme.border_strong else theme.border, 4);
const color = if (disabled != null) theme.text_off else if (lit) theme.text_strong else theme.text_muted;
const color = if (shown.disabled != null) theme.text_off else if (lit) theme.text_strong else theme.text_muted;
const middle = at.y + height / 2;
drawIcon(list, .{ .x = at.x + px(9), .y = middle }, icon, icon_size, color);
gui.drawText(list, .{ .x = at.x + px(9) + px(icon_size) + px(6), .y = gui.textTop(middle) }, color, label);
if (disabled) |off| gui.tooltip(off.reason);
if (shown.icon_only) {
drawIconCentered(list, .{ .x = at.x + width / 2, .y = middle }, shown.icon, ToggleChip.icon_size, color);
gui.tooltip(if (shown.disabled) |off| gui.print("{s} · {s}", .{ shown.label, off.reason }) else shown.label);
return clicked;
}
drawIcon(list, .{ .x = at.x + px(ToggleChip.padding), .y = middle }, shown.icon, ToggleChip.icon_size, color);
gui.sans(.regular, fonts.size.small);
defer gui.popFont();
gui.drawText(list, .{ .x = at.x + px(ToggleChip.padding + ToggleChip.icon_size + ToggleChip.gap), .y = gui.textTop(middle) }, color, shown.label);
if (shown.disabled) |off| gui.tooltip(off.reason);
return clicked;
}

Expand Down
130 changes: 103 additions & 27 deletions src/views/scene.zig
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,12 @@ const least_across = 200;
const most_across = 1e9;
/// The places a drag with Shift held snaps to, in the game's units.
const snap = 100;
/// How wide Follow's list is, at most and at least, the gap between the toolbar's chips, and the
/// least gap between them and Follow, in design pixels.
const most_follow_width = 120;
const least_follow_width = 64;
const chip_gap = 6;
const follow_gap = 12;

pub fn draw(app: *App) void {
const document = app.document orelse return;
Expand Down Expand Up @@ -192,7 +198,8 @@ fn lower(text: []const u8) []const u8 {
return std.ascii.lowerString(gui.frameAllocator().alloc(u8, text.len) catch return text, text);
}

/// The map's toolbar: the projection, what it shows, the record it follows, and Fit.
/// The map's toolbar: the projection, what it shows, the record it follows, and Fit, as room
/// allows (`ToolbarFit`). The chips that don't fit even without Follow are left out.
fn toolbar(document: *Document, scene: game.Scene) void {
const at = gui.cursor();
const width = gui.available().x;
Expand All @@ -215,39 +222,90 @@ fn toolbar(document: *Document, scene: game.Scene) void {
gui.rect(list, .{ .x = x, .y = middle - px(9) }, .{ .x = x + px(1), .y = middle + px(9) }, theme.border, 0);
x += px(9);
const live_planned: widgets.Disabled = .{ .reason = gui.print("Planned · {f}", .{commands.plans.live_scene}) };
const chips = [_]struct { []const u8, []const u8, Icon, ?*bool }{
.{ "live", "Live", .radio, null },
.{ "ranges", "Ranges", .circle_dashed, &Map.ranges },
.{ "routes", "Routes", .route, &Map.routes },
.{ "orders", "Orders", .arrow_up_right, null },
.{ "curves", "Curves", .spline, &Map.curves },
const chips = [_]struct { []const u8, widgets.ToggleChip, ?*bool }{
.{ "live", .{ .label = "Live", .icon = .radio, .disabled = live_planned }, null },
.{ "ranges", .{ .label = "Ranges", .icon = .circle_dashed, .on = Map.ranges }, &Map.ranges },
.{ "routes", .{ .label = "Routes", .icon = .route, .on = Map.routes }, &Map.routes },
.{ "orders", .{ .label = "Orders", .icon = .arrow_up_right, .disabled = live_planned }, null },
.{ "curves", .{ .label = "Curves", .icon = .spline, .on = Map.curves }, &Map.curves },
};

// At the end: Fit, and before it Follow's list and its label.
const fit_width = px(30);
const fit_left = at.x + width - px(10) - fit_width;
const follow_end = fit_left - px(6);
const follow_label = "Follow";
gui.sans(.regular, fonts.size.small);
const label_width = gui.textSize(follow_label).x + px(6);
gui.popFont();
// The chips' widths with their labels and with their icons alone, and the planned chips' share.
var labelled = px(chip_gap * (chips.len - 1));
var icons = labelled;
var planned: f32 = 0;
for (chips) |chip| {
labelled += chip[1].width();
icons += px(widgets.ToggleChip.icon_only_width);
if (chip[1].disabled != null) planned += px(widgets.ToggleChip.icon_only_width + chip_gap);
}
const fit: ToolbarFit = .of(follow_end - x - px(follow_gap), labelled, icons, planned, label_width);
const follow_left = if (fit.follow_width) |follow| follow_end - follow else fit_left;
const chips_end = follow_left - (if (fit.follow_label) label_width else 0) - px(follow_gap);

for (chips) |chip| {
var shown = chip[1];
shown.icon_only = !fit.chip_labels;
if (shown.disabled != null and !fit.planned_chips) continue;
if (x + shown.width() > chips_end) break;
gui.setCursor(.{ .x = x, .y = middle - px(12) });
if (widgets.toggleChip(chip[0], chip[1], chip[2], if (chip[3]) |on| on.* else false, if (chip[3] == null) live_planned else null)) {
if (chip[3]) |on| on.* = !on.*;
if (widgets.toggleChip(chip[0], shown)) {
if (chip[2]) |on| on.* = !on.*;
}
x = gui.lastItemRight() + px(6);
x = gui.lastItemRight() + px(chip_gap);
}

// At the end: Follow, and Fit.
const fit_width = px(30);
const follow_width = px(120);
var right = at.x + width - px(10) - fit_width;
gui.setCursor(.{ .x = right, .y = middle - px(13) });
gui.setCursor(.{ .x = fit_left, .y = middle - px(13) });
if (widgets.toolButton("fit", .{ .icon = .scan, .icon_only = true, .height = 26, .tooltip = "Zoom to fit the mission" })) Map.fit = true;
right -= px(6) + follow_width;
gui.setCursor(.{ .x = right, .y = middle - px(13) });
followPick(document, scene, follow_width);
gui.sans(.regular, fonts.size.small);
const follow_label = "Follow";
gui.drawText(list, .{ .x = right - px(6) - gui.textSize(follow_label).x, .y = gui.textTop(middle) }, theme.text_dim, follow_label);
gui.popFont();
if (fit.follow_width) |follow| {
gui.setCursor(.{ .x = follow_left, .y = middle - px(13) });
followPick(document, scene, follow, if (fit.follow_label) null else follow_label);
}
if (fit.follow_label) {
gui.sans(.regular, fonts.size.small);
gui.drawText(list, .{ .x = follow_left - label_width, .y = gui.textTop(middle) }, theme.text_dim, follow_label);
gui.popFont();
}
gui.setCursor(.{ .x = at.x, .y = at.y + height });
}

/// Follow's list: none, or a ship, which the map then keeps in its middle.
fn followPick(document: *Document, scene: game.Scene, width: f32) void {
/// What the map's toolbar shows in `room` pixels between the projection's switch and Fit. Where it
/// has no room for everything, it leaves out, in turn: the chips' labels, Follow's label, the chips
/// of what's planned, room in Follow's list down to its least width, and then Follow.
const ToolbarFit = struct {
chip_labels: bool = true,
planned_chips: bool = true,
follow_label: bool = true,
/// Follow's list's width, or null where Follow is left out.
follow_width: ?f32,

/// The fit for chips `labelled` wide with their labels and `icons` wide with their icons
/// alone, of which the planned chips take `planned`, and Follow's label `label` wide.
fn of(room: f32, labelled: f32, icons: f32, planned: f32, label: f32) ToolbarFit {
const most = px(most_follow_width);
if (labelled + label + most <= room) return .{ .follow_width = most };
if (icons + label + most <= room) return .{ .chip_labels = false, .follow_width = most };
if (icons + most <= room) return .{ .chip_labels = false, .follow_label = false, .follow_width = most };
const unplanned = icons - planned;
return .{
.chip_labels = false,
.planned_chips = false,
.follow_label = false,
.follow_width = if (unplanned + px(least_follow_width) <= room) @min(room - unplanned, most) else null,
};
}
};

/// Follow's list: none, or a ship, which the map then keeps in its middle. `tooltip` names it
/// where its label is left out.
fn followPick(document: *Document, scene: game.Scene, width: f32, tooltip: ?[]const u8) void {
const known = document.outline();
const allocator = gui.frameAllocator();
var choices: std.ArrayList(widgets.Choice) = .empty;
Expand All @@ -265,9 +323,9 @@ fn followPick(document: *Document, scene: game.Scene, width: f32) void {
};
}
const frame: widgets.Frame = if (current) |record|
.{ .label = record.title, .mark = .{ .icon = .{ .icon = .navigation_2, .color = panels.groupColor(record.*) } }, .width = width }
.{ .label = record.title, .mark = .{ .icon = .{ .icon = .navigation_2, .color = panels.groupColor(record.*) } }, .width = width, .tooltip = tooltip }
else
.{ .label = "none", .dim_label = true, .width = width };
.{ .label = "none", .dim_label = true, .width = width, .tooltip = tooltip };
if (widgets.pick("follow", frame, choices.items)) |index| Map.follow = refs.items[index];
}

Expand Down Expand Up @@ -875,6 +933,24 @@ fn placeNoteOf(scene: game.Scene, projection: Projection, mark: Mark) drawing.Pl
return .at(mark.middle, axisNames(projection), .{ place[axes[0]], place[axes[1]] });
}

test ToolbarFit {
// Chips 400 wide with their labels and 184 with their icons alone, 76 of it the planned chips',
// and Follow's label 46 wide.
const fit = struct {
fn of(room: f32) ToolbarFit {
return .of(room, 400, 184, 76, 46);
}
}.of;
try std.testing.expectEqual(ToolbarFit{ .follow_width = 120 }, fit(600));
try std.testing.expectEqual(ToolbarFit{ .chip_labels = false, .follow_width = 120 }, fit(500));
try std.testing.expectEqual(ToolbarFit{ .chip_labels = false, .follow_label = false, .follow_width = 120 }, fit(320));
// Then the planned chips go, and Follow's list narrows down to its least width.
try std.testing.expectEqual(ToolbarFit{ .chip_labels = false, .planned_chips = false, .follow_label = false, .follow_width = 120 }, fit(250));
try std.testing.expectEqual(ToolbarFit{ .chip_labels = false, .planned_chips = false, .follow_label = false, .follow_width = 92 }, fit(200));
// Below that, Follow goes.
try std.testing.expectEqual(ToolbarFit{ .chip_labels = false, .planned_chips = false, .follow_label = false, .follow_width = null }, fit(150));
}

test isOnAnyRoute {
const legs = [_]game.Leg{.{ .from = 0, .to = 1, .way = .fly }};
const routes = [_]game.Route{
Expand Down
Loading