Backlog/engine/papyrus/src/papyrus.zig

1308 lines
45 KiB
Zig

const std = @import("std");
const c = @cImport({
@cInclude("stb_ttf.h");
});
pub const Layout = @import("MousePick.zig");
pub const Event = @import("Event.zig");
pub const Font = @import("Font.zig");
pub const FontAtlas = Font.FontAtlas;
pub const BmpRenderer = @import("BmpRenderer.zig");
pub const BmpWriter = BmpRenderer.BmpWriter;
pub const style = @import("style.zig");
pub const ModernStyle = style.ModernStyle;
pub const BurnStyle = style.BurnStyle;
pub const utils = @import("utils.zig");
pub const FileLog = utils.FileLog;
pub const grapvizDotToPng = utils.grapvizDotToPng;
pub const loadFileAlloc = utils.loadFileAlloc;
pub const assertf = utils.assertf;
pub const localization = @import("localization.zig");
pub const LocText = localization.LocText;
pub const MakeText = localization.MakeText;
pub const HandlerError = Event.HandlerError;
pub const PressedType = Event.PressedType;
pub const TextRenderGeometry = @import("textRender/textRenderGeometry.zig");
pub const DrawListBuilder = @import("DrawListBuilder.zig");
pub const NodeProperty_Button = @import("primitives/button.zig");
pub const NodeProperty_TextEntry = @import("primitives/textEntry.zig");
pub const TextEntrySystem = @import("TextEntrySystem.zig");
pub const DrawCommand = @import("DrawCommand.zig");
pub const DrawList = std.ArrayList(DrawCommand);
const core = @import("core");
const colors = core.colors;
pub const Color = colors.Color;
pub const ColorRGBA8 = colors.RGBA8;
const Vector2i = core.Vector2i;
const Vector2f = core.Vector2f;
const IndexPool = core.IndexPool;
const Name = core.Name;
const asserts = core.asserts;
pub const NodeHandle = core.IndexPoolHandle;
// Mixed mode implementation agnostic UI library
//
// Intended for use with real time applications.
// This file only contains the core layout engine and events pump for the IO processor.
//
// Actual hook up of the IO Processor and graphics is done elsewhere.
// =================================== Document: How should the layout engine work ===========================
//
// In terms of goals I want to be able to support both screen space proportional UIs as well as UIs for
// creating general purpose applications.
//
//
// Notes on some other applications like remedybg.
//
// - Panels are either free floating or docked
// - Their position is described as an anchor offset
// - Elements have a minimum size which they are happy to shrink to.
//
// Docked:
// - when docked, the panel's size layout is described as a percentage of their parent's docking space
// - Text should stay the same and not proportionally follow the window.
// - Images and image based layouts should.
//
// Free:
//
// - Their size is absolute, and their position is relative to the reference anchor.
//
// Text:
// - Text should build their geometry requirements ahead of time, and only be looked up when we're ready to render.
// - For a given length of text and a maximum width, we should be able to figure out it's height requirements.
//
// Tasks:
// - Panel Layout
// - internal layout:
// - free:
// - grid:
// - verticalList:
// - horizontalList:
//
// - Layout algorithm
// - for each node
// - get parent context
// - create current layout context
// - call into sub layout function with current context
//
// =================================== /Document: How should the layout engine work ===========================
pub const AnchorNode = enum {
// zig fmt: off
Free, // Anchored to 0,0 absolute on the screen
TopLeft, // Anchored such that 0,0 corresponds to the top left corner of the parent node
MidLeft, // Anchored such that 0,0 corresponds to the left edge midpoint of the parent node
BotLeft, // Anchored such that 0,0 corresponds to the bottom left corner of the parent node
TopMiddle, // Anchored such that 0,0 corresponds to the top edge midpoint of the parent node
MidMiddle, // Anchored such that 0,0 corresponds to the center of the parent node
BotMiddle, // Anchored such that 0,0 corresponds to the bottom edge midpoint of the parent node
TopRight, // Anchored such that 0,0 corresponds to the top right corner of the parent node
MidRight, // Anchored such that 0,0 corresponds to the right edge midpoint of the parent node
BotRight, // Anchored such that 0,0 corresponds to the bottom right corner of the parent node
// zig fmt: on
};
pub const FillMode = enum {
// zig fmt: off
None, // Size will be absolute as specified by content
FillX, // Size will scale to X scaling of the parent (relative to reference), if set, then the size value of x will be interpreted as a percentage of the parent
FillY, // Size will scale to Y scaling of the parent (relative to reference), if set, then the size value of y will be interpreted as a percentage of the parent
FillXY, // Size will scale to both X and Y of the parent (relative to reference), if set, then the size value of both x and y will be interpreted as a percentage of the parent
UniformX, // Size will scale both X and Y based on a ratio of the parent's current X vs the parent's original X
UniformY, // Size will scale both X and Y based on a ratio of the parent's current Y vs the parent's original Y
// zig fmt: on
};
// When this element is used as part of a VBox it's left/right position
// shall be arranged according this justificiation value
pub const NodeJustify = enum {
// zig fmt: off
Left,
Center,
Right,
// zig fmt: on
};
pub const HitTestability = enum {
Testable,
NotTestable,
};
pub const State = enum {
Visible,
Collapsed,
Hidden,
};
pub const ChildLayout = enum { Free, Vertical, Horizontal };
pub const NodeProperty_Slot = struct {
layoutMode: ChildLayout = .Free,
};
pub const NodeProperty_Text = struct {
textSize: f32,
color: Color,
font: Font,
};
pub const NodeProperty_Panel = struct {
titleColor: Color = ModernStyle.GreyDark,
titleSize: f32 = 20,
layoutMode: ChildLayout = .Free, // when set to anything other than free, we will override anchors from inferior nodes.
hasTitle: bool = false,
font: *FontAtlas,
useImage: bool = false,
imageReference: core.Name, // Image resource reference
rounding: struct {
tl: f32 = 0,
tr: f32 = 0,
bl: f32 = 0,
br: f32 = 0,
} = .{}, // rounding
};
pub const NodePropertiesBag = union(enum(u8)) {
Slot: NodeProperty_Slot,
DisplayText: NodeProperty_Text,
Button: NodeProperty_Button,
Panel: NodeProperty_Panel,
TextEntry: NodeProperty_TextEntry,
};
pub const NodePadding = union(enum(u8)) {
topLeft: Vector2f,
botLeft: Vector2f,
all: f32,
allSides: [2]Vector2f,
};
pub const StyleInvisible = NodeStyle{
.foregroundColor = .{ .r = 0, .g = 0, .b = 0, .a = 0 },
.backgroundColor = .{ .r = 0, .g = 0, .b = 0, .a = 0 },
.borderColor = .{ .r = 0, .g = 0, .b = 0, .a = 0 },
.borderWidth = 0.0,
};
pub const NodeStyle = struct {
foregroundColor: Color = BurnStyle.Normal,
backgroundColor: Color = BurnStyle.SlateGrey,
borderColor: Color = BurnStyle.Bright2,
borderWidth: f32 = 1.1,
};
pub const TextRenderMode = enum {
Simple,
NoControl,
Rich,
};
// this is going to follow a pretty object-oriented-like user facing api
pub const Node = struct {
text: LocText = MakeText("hello world"),
textMode: TextRenderMode = .Simple,
textRenderedSize: Vector2f = .{ .x = 0.0, .y = 0.0 },
parent: NodeHandle = .{}, // 0 corresponds to true root
// all children of the same parent operate as a doubly linked list
child: NodeHandle = .{},
end: NodeHandle = .{},
next: NodeHandle = .{},
prev: NodeHandle = .{},
zOrder: i32 = 0, // higher number is rendered on top.
// Not sure what's the best way to go about this.
// I want to provide good design time metrics
// which can scale into good runtime metrics
// Resolutions and scalings are a real headspinner
// DPI awareness and content scaling is also a huge problem.
baseSize: Vector2f = .{ .x = 0, .y = 0 },
sizeInitialized: bool = false,
size: Vector2f = .{ .x = 0, .y = 0 },
justify: NodeJustify = .Center,
pos: Vector2f = .{ .x = 0, .y = 0 },
anchor: AnchorNode = .TopLeft,
fill: FillMode = .None,
layoutPadding: f32 = 5.0,
// padding is the external padding factor
padding: NodePadding = .{ .all = 0 },
state: State = .Visible,
hittest: HitTestability = .Testable,
dockingPolicy: enum { Dockable, NotDockable } = .Dockable,
// standard styling options that all nodes have
style: NodeStyle = .{},
nodeType: NodePropertiesBag,
pub fn setSize(self: *@This(), s: Vector2f) void {
if (!self.sizeInitialized) {
self.baseSize = s;
self.sizeInitialized = true;
}
self.size = s;
}
};
// final resolved size
pub const LayoutInfo = struct {
baseSize: Vector2f,
pos: Vector2f,
size: Vector2f,
childLayoutOffsets: Vector2f,
};
pub const HorizontalLayoutInfo = struct {
horizontalChildSize: f32 = 0,
};
pub const FontCache = struct {
allocator: std.mem.Allocator,
fonts: std.AutoHashMapUnmanaged(u32, Font),
defaultFont: Font,
defaultMonoFont: Font,
defaultBitmapFont: Font,
pub fn create(allocator: std.mem.Allocator) !*@This() {
const self = try allocator.create(@This());
const defaultFontName: []const u8 = "default";
const defaultMonoName: []const u8 = "monospace";
const defaultBitmapFontName: []const u8 = "bitmap";
self.* = .{
.allocator = allocator,
.fonts = .{},
.defaultFont = Font{
.name = Name.fromUtf8(defaultFontName),
.atlas = try allocator.create(FontAtlas),
},
.defaultMonoFont = Font{
.name = Name.fromUtf8(defaultMonoName),
.atlas = try allocator.create(FontAtlas),
},
.defaultBitmapFont = Font{
.name = Name.fromUtf8(defaultBitmapFontName),
.atlas = try allocator.create(FontAtlas),
},
};
self.defaultFont.atlas.* = try FontAtlas.initDefaultFont(allocator, 64);
try self.installFontAtlas(self.defaultFont.name.utf8(), self.defaultFont.atlas);
self.defaultMonoFont.atlas.* = try FontAtlas.initMonoFont(allocator, 64);
try self.installFontAtlas(self.defaultMonoFont.name.utf8(), self.defaultMonoFont.atlas);
// this is a 16 px sized default font
self.defaultBitmapFont.atlas.* = try FontAtlas.initDefaultBitmapFont(allocator, 16);
try self.installFontAtlas(self.defaultBitmapFont.name.utf8(), self.defaultBitmapFont.atlas);
// difference between asserts and assertf is asserts is not recoverable,
// instantly crashes.
asserts(
self.defaultBitmapFont.atlas.atlasBuffer != null,
"expected atlas buffer to be valid {any}",
.{self.defaultBitmapFont.atlas.atlasBuffer},
@src().fn_name,
);
return self;
}
pub fn installFontAtlas(self: *@This(), fontName: []const u8, atlas: *FontAtlas) !void {
var name = Name.fromUtf8(fontName);
try self.fonts.put(self.allocator, name.handle(), .{ .atlas = atlas, .name = name });
}
pub fn destroy(self: *@This()) void {
var iter = self.fonts.iterator();
while (iter.next()) |i| {
i.value_ptr.atlas.deinit();
self.allocator.destroy(i.value_ptr.atlas);
}
self.fonts.deinit(self.allocator);
self.allocator.destroy(self);
}
};
pub const PapyrusRuntime = struct {
allocator: std.mem.Allocator,
fontCache: *FontCache,
pub fn create(allocator: std.mem.Allocator) !*@This() {
const self = try allocator.create(@This());
self.* = .{
.allocator = allocator,
.fontCache = try FontCache.create(allocator),
};
return self;
}
pub fn addContext(self: *@This()) !*Context {
const rv = try Context.create(self.allocator, self);
return rv;
}
pub fn destroy(self: *@This()) void {
self.fontCache.destroy();
self.allocator.destroy(self);
}
};
pub const Context = struct {
nodes: IndexPool(Node),
allocator: std.mem.Allocator,
extent: Vector2i = .{ .x = 1920, .y = 1080 },
currentCursorPosition: Vector2f = .{},
styleStack: std.ArrayListUnmanaged(NodeStyle) = .{},
resolvedStyle: NodeStyle = .{},
defaultFont: Font,
defaultMonoFont: Font,
defaultBitmapFont: Font,
mousePick: Layout,
fontCache: *FontCache,
events: Event,
textEntry: *TextEntrySystem,
// internals
_drawOrder: DrawOrderList,
_layoutNodes: std.ArrayListUnmanaged(NodeHandle),
_layout: std.ArrayListUnmanaged(LayoutInfo), // bad name, this is used for hittest, not display
_displayLayout: std.ArrayListUnmanaged(LayoutInfo) = .{},
_horizontalLayout: std.AutoHashMapUnmanaged(NodeHandle, HorizontalLayoutInfo) = .{},
debugText: std.ArrayList([]u8),
debugTextCount: u32 = 0,
drawDebug: bool = false,
const debugTextMax = 32;
pub fn tick(self: *@This(), deltaTime: f64) !void {
self.clearDebugText();
try self.mousePick.tick(self, deltaTime);
try self.textEntry.tick(deltaTime);
try self.tickDebug(deltaTime);
}
pub fn tickDebug(self: *@This(), deltaTime: f64) !void {
_ = deltaTime;
try self.pushDebugText("mouse Position: {d}, {d}", .{ self.currentCursorPosition.x, self.currentCursorPosition.y });
if (self.mousePick.selected_node) |node| {
const n = self.getRead(node);
const layout = self._displayLayout.items[node.index];
try self.pushDebugText("found node: {d},{d} size={d}x{d} layoutpos={d},{d} layoutsize={d},{d}", .{
node.index,
node.generation,
n.size.x,
n.size.y,
layout.pos.x,
layout.pos.y,
layout.size.x,
layout.size.y,
});
}
}
pub fn create(backingAllocator: std.mem.Allocator, ctx: *PapyrusRuntime) !*@This() {
var self = try backingAllocator.create(@This());
var allocator = backingAllocator;
self.* = .{
.allocator = allocator,
.nodes = IndexPool(Node).init(allocator),
.events = Event.init(allocator),
.fontCache = ctx.fontCache,
.defaultFont = ctx.fontCache.defaultFont,
.defaultMonoFont = ctx.fontCache.defaultMonoFont,
.defaultBitmapFont = ctx.fontCache.defaultBitmapFont,
.textEntry = try TextEntrySystem.create(self, allocator),
._drawOrder = DrawOrderList.init(allocator),
._layout = .{},
._layoutNodes = .{},
._horizontalLayout = .{},
.debugText = std.ArrayList([]u8).init(allocator),
.mousePick = Layout.init(allocator),
};
for (0..debugTextMax) |_| {
const textBuffer = try allocator.alloc(u8, 512);
try self.debugText.append(textBuffer);
}
// constructing the root node
_ = try self.nodes.new(.{
.text = MakeText("root"),
.parent = .{},
.nodeType = .{ .Slot = .{} },
});
try self.pushDebugText("mouse position: {d}, {d}", .{ 0, 0 });
return self;
}
pub fn pushDebugText(self: *@This(), comptime fmt: []const u8, args: anytype) !void {
if (self.debugTextCount < debugTextMax) {
_ = try std.fmt.bufPrintZ(self.debugText.items[self.debugTextCount], fmt, args);
self.debugTextCount += 1;
}
}
pub fn clearDebugText(self: *@This()) void {
self.debugTextCount = 0;
}
pub const destroy = deinit;
pub fn deinit(self: *@This()) void {
for (0..self.nodes.count()) |i| {
const maybeNode = self.nodes.indexToHandle(i);
if (maybeNode) |node| {
const n = self.nodes.active.items[i];
if (n.?.nodeType == .TextEntry) {
NodeProperty_TextEntry.tearDown(self, node);
}
}
}
self.mousePick.deinit();
self.textEntry.destroy();
self.nodes.deinit();
self.events.deinit();
self._drawOrder.deinit();
self._layout.deinit(self.allocator);
self._layoutNodes.deinit(self.allocator);
self._horizontalLayout.deinit(self.allocator);
// self._layoutPositions.deinit(self.allocator);
self._displayLayout.deinit(self.allocator);
for (self.debugText.items) |text| {
self.allocator.free(text);
}
self.debugText.deinit();
self.allocator.destroy(self);
}
pub fn fetchPanel(self: *@This(), handle: NodeHandle) ?*NodeProperty_Panel {
if (self.nodes.get(handle)) |node| {
switch (node.nodeType) {
.Panel => {
return &(node.nodeType.Panel);
},
else => {
return null;
},
}
}
return null;
}
pub fn isValid(self: @This(), handle: NodeHandle) bool {
return self.nodes.isValid(handle);
}
pub fn getPanel(self: *@This(), handle: NodeHandle) *NodeProperty_Panel {
return &(self.nodes.get(handle).?.nodeType.Panel);
}
pub fn getButton(self: *@This(), handle: NodeHandle) *NodeProperty_Button {
return &(self.nodes.get(handle).?.nodeType.Button);
}
pub fn getText(self: *@This(), handle: NodeHandle) *NodeProperty_Text {
return &(self.nodes.get(handle).?.nodeType.DisplayText);
}
pub fn getTextEntry(self: *@This(), node: NodeHandle) *NodeProperty_TextEntry {
return &(self.nodes.get(node).?.nodeType.TextEntry);
}
pub fn setFont(self: *@This(), handle: NodeHandle, font: []const u8) void {
var name = Name.fromUtf8(font);
switch (self.nodes.get(handle).?.nodeType) {
.DisplayText => {
var text = &(self.nodes.get(handle).?.nodeType.DisplayText);
text.font = .{
.name = name,
.atlas = (self.fonts.get(name.handle())).?.atlas,
};
},
.Panel => {
var panel = &(self.nodes.get(handle).?.nodeType.Panel);
panel.font = self.fonts.get(name.handle()).?.atlas;
},
.TextEntry => {
self.getTextEntry(handle).font = self.fonts.get(name.handle()) orelse self.defaultFont;
},
else => {},
}
}
// converts a handle to a node pointer
// invalid after AddNode
pub fn get(self: *@This(), handle: NodeHandle) *Node {
return self.nodes.get(handle).?;
}
// gets a node as read only
pub fn getRead(self: @This(), handle: NodeHandle) *const Node {
if (!self.isValid(handle))
@panic("handle is invalid");
return self.nodes.getRead(handle).?;
}
fn newNode(self: *@This(), node: Node) !NodeHandle {
return try self.nodes.new(node);
}
pub fn addSlot(self: *@This(), parent: NodeHandle) !NodeHandle {
const slotNode = Node{ .nodeType = .{ .Slot = .{} } };
const slot = try self.newNode(slotNode);
try self.setParent(slot, parent);
return slot;
}
pub fn addText(self: *@This(), parent: NodeHandle, text: []const u8) !NodeHandle {
const slotNode = Node{
.text = LocText.fromUtf8(text),
.nodeType = .{ .DisplayText = .{
.textSize = 24,
.color = Color.White,
.font = self.defaultFont,
} },
};
const slot = try self.newNode(slotNode);
try self.setParent(slot, parent);
return slot;
}
pub fn addPanel(self: *@This(), parent: NodeHandle) !NodeHandle {
var slotNode = Node{ .nodeType = .{ .Panel = .{
.font = self.defaultFont.atlas,
.imageReference = core.NameInvalid,
} } };
if (parent.index == 0) {
slotNode.anchor = .Free;
}
const slot = try self.nodes.new(slotNode);
try self.setParent(slot, parent);
return slot;
}
pub fn setParent(self: *@This(), node: NodeHandle, parent: NodeHandle) !void {
try assertf(
self.nodes.get(parent) != null,
"tried to assign node {d}.{d} to parent {d}.{d} but parent does not exist",
.{ node.index, node.generation, parent.index, parent.generation },
);
try assertf(
self.nodes.get(node) != null,
"tried to assign node {d}.{d} to parent {d}.{d} but node does not exist",
.{ node.index, node.generation, parent.index, parent.generation },
);
const parentNode = self.nodes.get(parent).?;
const thisNode = self.nodes.get(node).?;
// if we have a previous parent, remove ourselves
if (thisNode.*.parent.index != 0) {
// remove myself from that parent.
const oldParentNode = self.nodes.get(thisNode.*.parent).?;
// special case for when we're the first child element
if (oldParentNode.*.child.index == node.index) {
oldParentNode.*.child = thisNode.next;
} else {
// otherwise remove ourselves from the linked list
self.nodes.get(thisNode.prev).?.*.next = thisNode.next;
}
}
// assign ourselves the new parent
self.nodes.get(node).?.*.parent = parent;
if (parentNode.*.child.index == 0) {
parentNode.*.child = node;
} else {
self.nodes.get(parentNode.*.end).?.*.next = node;
thisNode.*.prev = parentNode.*.end;
}
parentNode.*.end = node;
}
pub fn addTextEntry_experimental(self: *@This(), parent: NodeHandle, text: ?[]const u8) !NodeHandle {
var textProperty = NodeProperty_TextEntry{
.editText = std.ArrayList(u8).init(self.allocator),
.font = self.defaultFont,
};
if (text) |t| {
try textProperty.editText.appendSlice(t);
} else {
try textProperty.editText.appendSlice("click to edit...");
}
const n = try self.nodes.new(.{ .nodeType = .{ .TextEntry = textProperty } });
try self.setParent(n, parent);
self.get(n).size = .{ .x = 350, .y = 30 };
try self.events.installOnPressedEventAdvanced(n, .onPressed, .Mouse1, null, NodeProperty_TextEntry.onPressedEvent, true);
return n;
}
// helper functions for a whole bunch of shit
pub fn addButton(self: *@This(), parent: NodeHandle, text: ?[]const u8) !NodeHandle {
const buttonProperty = NodeProperty_Button{
.font = self.defaultFont,
};
const button = try self.nodes.new(.{ .nodeType = .{ .Button = buttonProperty } });
try self.setParent(button, parent);
self.get(button).size = .{ .x = 100, .y = 50 };
if (text) |t| {
self.get(button).text = LocText.fromUtf8(t);
}
try self.events.installMouseOverEventAdvanced(button, .mouseOver, null, NodeProperty_Button.buttonMouseOverListener, true);
try self.events.installMouseOverEventAdvanced(button, .mouseOff, null, NodeProperty_Button.buttonMouseOffListener, true);
try self.events.installOnPressedEventAdvanced(button, .onPressed, .Mouse1, null, NodeProperty_Button.buttonOnPressedEvent, true);
try self.events.installOnPressedEventAdvanced(button, .onReleased, .Mouse1, null, NodeProperty_Button.buttonOnPressedEvent, true);
return button;
}
pub fn pushStyle(self: *@This(), node: NodeStyle) void {
_ = node;
_ = self;
}
// removes this node from its' parent as well as purges all subnodes.
pub fn removeFromParent(self: *@This(), node: NodeHandle) !void {
// this also deletes all children
// 1. gather all children.
if (!self.nodes.isValid(node)) {
return;
}
try assertf(node.index != 0, "removeFromParent CANNOT be called on the root node", .{});
var killList = std.ArrayList(NodeHandle).init(self.allocator);
defer killList.deinit();
self.walkNodesToRemove(node, &killList) catch {
for (killList.items) |n| {
_ = n;
}
return error.BadWalk;
};
const thisNode = self.getRead(node);
self.get(thisNode.next).prev = thisNode.prev;
self.get(thisNode.prev).next = thisNode.next;
{
var parent = self.get(thisNode.parent);
if (thisNode.nodeType == .TextEntry) {
NodeProperty_TextEntry.tearDown(self, node);
}
if (parent.child.eql(node)) {
parent.child = thisNode.next;
}
if (parent.end.eql(node)) {
parent.end = thisNode.prev;
}
}
for (killList.items) |killed| {
self.nodes.destroy(killed);
}
// Uninstall handlers from the events system
self.events.uninstallAllEvents_OnDestroy(node);
}
fn walkNodesToRemove(self: @This(), root: NodeHandle, killList: *std.ArrayList(NodeHandle)) !void {
try killList.append(root);
var next = self.getRead(root).child;
while (next.index != 0) {
try self.walkNodesToRemove(next, killList);
next = self.getRead(next).next;
}
}
// ============================= Rendering and Layout ==================
const DrawOrderList = std.ArrayList(NodeHandle);
fn assembleDrawOrderListForNode(self: @This(), node: NodeHandle, list: *DrawOrderList) !void {
var next: NodeHandle = self.getRead(node).child;
while (next.index != 0) : (next = self.getRead(next).next) {
if (self.getRead(next).state != .Visible) {
continue;
}
try list.append(next);
try self.assembleDrawOrderListForNode(next, list);
}
}
fn assembleDrawOrderList(self: @This(), list: *DrawOrderList) !void {
var rootNodes = std.ArrayList(NodeHandle).init(self.allocator);
defer rootNodes.deinit();
var next: NodeHandle = self.getRead(.{}).child;
while (next.index != 0) : (next = self.getRead(next).next) {
if (self.getRead(next).state != .Visible) {
continue;
}
try rootNodes.append(next);
}
const SortFunc = struct {
pub fn lessThan(ctx: Context, lhs: NodeHandle, rhs: NodeHandle) bool {
return ctx.getRead(lhs).zOrder < ctx.getRead(rhs).zOrder;
}
};
std.sort.insertion(
NodeHandle,
rootNodes.items,
self,
SortFunc.lessThan,
);
for (rootNodes.items) |rootNode| {
try list.append(rootNode);
try self.assembleDrawOrderListForNode(rootNode, list);
}
}
fn resolveAnchoredPosition(parent: LayoutInfo, node: *const Node) Vector2f {
switch (node.anchor) {
.Free => {
return node.pos;
},
.TopLeft => {
return parent.pos.add(node.pos);
},
.MidLeft => {
return parent.pos.add(.{ .y = parent.size.y / 2 }).add(node.pos);
},
.BotLeft => {
return parent.pos.add(.{ .y = parent.size.y }).add(node.pos);
},
.TopMiddle => {
return parent.pos.add(.{ .x = parent.size.x / 2 }).add(node.pos);
},
.MidMiddle => {
return parent.pos.add(.{ .x = parent.size.x / 2, .y = parent.size.y / 2 }).add(node.pos);
},
.BotMiddle => {
return parent.pos.add(.{ .x = parent.size.x / 2, .y = parent.size.y }).add(node.pos);
},
.TopRight => {
return parent.pos.add(.{ .x = parent.size.x }).add(node.pos);
},
.MidRight => {
return parent.pos.add(.{ .x = parent.size.x, .y = parent.size.y / 2 }).add(node.pos);
},
.BotRight => {
return parent.pos.add(.{ .x = parent.size.x, .y = parent.size.y }).add(node.pos);
},
}
}
pub fn onKey(self: *@This(), keycode: Event.Key, eventType: Event.PressedType) !void {
if (self.mousePick.selected_node) |node|
try self.events.pushPressedEvent(node, eventType, keycode);
}
fn resolveAnchoredSize(parent: LayoutInfo, node: *const Node) Vector2f {
switch (node.fill) {
.None => {
return node.size;
},
.FillX => {
return .{ .x = node.size.x * parent.size.x / parent.baseSize.x, .y = node.size.y };
},
.FillY => {
return .{ .x = node.size.x, .y = node.size.y * parent.size.y / parent.baseSize.y };
},
.FillXY => {
return node.size.vmul(parent.size);
},
.UniformX => {
return node.size.fmul(parent.size.x / parent.baseSize.x);
},
.UniformY => {
return node.size.fmul(parent.size.y / parent.baseSize.y);
},
}
}
const ChildLayoutRulesStruct = struct {
position: Vector2f,
size: Vector2f,
};
pub fn applyLayoutRulesAsChild(
self: *@This(),
parentAsPanel: *NodeProperty_Panel,
n: *const Node,
resolvedSize: Vector2f,
resolvedPos: Vector2f,
) ChildLayoutRulesStruct {
_ = resolvedPos;
var parentInfo = &self._displayLayout.items[n.parent.index];
switch (parentAsPanel.layoutMode) {
.Vertical => {
var offsetX: f32 = 0.0;
switch (n.justify) {
.Left => {},
.Center => {
offsetX = @divFloor(parentInfo.size.x - resolvedSize.x, 2);
},
.Right => {
offsetX = parentInfo.size.x - resolvedSize.x + offsetX - 1;
},
}
const rv = ChildLayoutRulesStruct{
.position = parentInfo.pos.add(.{
.x = offsetX,
.y = parentInfo.childLayoutOffsets.y,
}),
.size = resolvedSize,
};
// Make one more adjustment to the offsetX position based on the justification.
// Justification is taken from the AnchorMode.
parentInfo.childLayoutOffsets.y += resolvedSize.y + self.get(n.parent).layoutPadding;
return rv;
},
.Horizontal => {
const horizontalLayout = self._horizontalLayout.getPtr(n.parent).?;
var offsetX: f32 = 0.0;
switch (n.justify) {
.Left => {},
.Center => {
offsetX = @divFloor(horizontalLayout.horizontalChildSize, 2);
},
.Right => {
offsetX = horizontalLayout.horizontalChildSize + offsetX - 1;
},
}
offsetX += parentInfo.childLayoutOffsets.x;
const rv = ChildLayoutRulesStruct{
.position = parentInfo.pos.add(.{
.x = offsetX,
.y = parentInfo.childLayoutOffsets.y,
}),
.size = resolvedSize,
};
// std.debug.print("{any}\n", .{parentInfo.pos});
parentInfo.childLayoutOffsets.x += resolvedSize.x + self.get(n.parent).layoutPadding;
return rv;
},
.Free => {},
}
return .{
.size = resolveAnchoredSize(parentInfo.*, n),
.position = resolveAnchoredPosition(parentInfo.*, n),
};
}
pub fn makeDrawList(self: *@This(), drawList: *DrawList, stringArena: *std.heap.ArenaAllocator) !void {
_ = stringArena.reset(.retain_capacity);
// do not re allocate these, instead use a preallocated pool
self._drawOrder.clearRetainingCapacity();
self._layout.clearRetainingCapacity();
self._layoutNodes.clearRetainingCapacity();
self._displayLayout.clearRetainingCapacity();
try self.assembleDrawOrderList(&self._drawOrder);
// todo: remove this layout hashmap, stash it in the main context.
try self._displayLayout.resize(self.allocator, self.nodes.count());
self._displayLayout.items[0] = .{
.pos = .{ .x = 0, .y = 0 },
.size = Vector2f.from(self.extent),
.baseSize = Vector2f.from(self.extent),
.childLayoutOffsets = .{},
};
drawList.clearRetainingCapacity();
for (self._drawOrder.items) |node| {
const n = self.getRead(node);
if (n.state == .Hidden) {
continue;
}
const parentInfo = self._displayLayout.items[n.parent.index];
var dlb = DrawListBuilder{
.ctx = self,
.node = node,
.drawList = drawList,
.n = n,
.parentInfo = self._displayLayout.items[n.parent.index],
.resolvedSize = resolveAnchoredSize(parentInfo, n),
.resolvedPos = resolveAnchoredPosition(parentInfo, n),
};
// if our parent is a panel then apply layout rules to it.
if (self.fetchPanel(n.parent)) |parentAsPanel| {
const results = self.applyLayoutRulesAsChild(parentAsPanel, n, dlb.resolvedSize, dlb.resolvedPos);
dlb.resolvedPos = results.position;
dlb.resolvedSize = results.size;
}
switch (n.nodeType) {
.Panel => |panel| {
try self._layout.append(self.allocator, .{
.baseSize = n.baseSize,
.pos = dlb.resolvedPos,
.size = dlb.resolvedSize,
.childLayoutOffsets = .{},
});
try self._layoutNodes.append(self.allocator, node);
if (panel.layoutMode == .Horizontal) {
// walk through all children and add up their horizontal layout sizes and
// this is a best guess for now, dont support flexible sizing here.
var iter = n.child;
var offset: f32 = 0;
var next = self.nodes.get(iter).?;
while (iter.index != 0) {
offset += next.size.x + n.layoutPadding;
iter = next.next;
next = self.nodes.get(iter).?;
}
try self._horizontalLayout.put(self.allocator, node, .{ .horizontalChildSize = offset });
}
if (panel.hasTitle) {
// draw the main image.
try drawList.append(.{ .node = node, .primitive = .{
.Rect = .{
.tl = dlb.resolvedPos.add(.{ .y = panel.titleSize }),
.size = dlb.resolvedSize.sub(.{ .y = panel.titleSize }),
.borderColor = n.style.borderColor,
.backgroundColor = n.style.backgroundColor,
.rounding = .{
.bl = panel.rounding.bl,
.br = panel.rounding.br,
},
.imageRef = if (panel.useImage) panel.imageReference else null,
},
} });
// draw the title bar
try drawList.append(.{ .node = node, .primitive = .{
.Rect = .{
.tl = dlb.resolvedPos,
.size = .{ .x = dlb.resolvedSize.x, .y = panel.titleSize },
.borderColor = n.style.borderColor,
.backgroundColor = n.style.borderColor,
.rounding = .{
.tl = panel.rounding.tl,
.tr = panel.rounding.tr,
},
},
} });
try drawList.append(.{ .node = node, .primitive = .{
.Text = .{
.tl = dlb.resolvedPos.add(.{ .x = 3 + 5, .y = 3 }),
.size = .{ .x = dlb.resolvedSize.x, .y = panel.titleSize },
.text = n.text,
.renderMode = n.textMode,
.color = panel.titleColor,
.textSize = panel.titleSize - 4,
.rendererHash = panel.font.rendererHash,
.flags = .{
.setSourceGeometry = false,
},
},
} });
self._displayLayout.items[node.index] = .{
.baseSize = n.baseSize,
.pos = dlb.resolvedPos.add(.{ .y = panel.titleSize }).add(Vector2f.Ones),
.size = dlb.resolvedSize.sub(.{ .y = -panel.titleSize }).sub(Vector2f.Ones).sub(.{ .x = 0, .y = 30 }),
.childLayoutOffsets = .{},
};
} else {
// draw the main image
try drawList.append(.{ .node = node, .primitive = .{
.Rect = .{
.tl = dlb.resolvedPos,
.size = dlb.resolvedSize,
.borderColor = n.style.borderColor,
.backgroundColor = n.style.backgroundColor,
.rounding = .{
.tl = panel.rounding.tl,
.tr = panel.rounding.tr,
.bl = panel.rounding.bl,
.br = panel.rounding.br,
},
.imageRef = if (panel.useImage) panel.imageReference else null,
},
} });
self._displayLayout.items[node.index] = .{
.baseSize = n.baseSize,
.pos = dlb.resolvedPos.add(Vector2f.Ones),
.size = dlb.resolvedSize.sub(Vector2f.Ones.fmul(2)),
.childLayoutOffsets = .{},
};
}
},
.DisplayText => |txt| {
try drawList.append(.{ .node = node, .primitive = .{
.Text = .{
.tl = dlb.resolvedPos,
.size = n.size,
.text = n.text,
.renderMode = n.textMode,
.color = n.style.foregroundColor,
.textSize = txt.textSize,
.rendererHash = txt.font.atlas.rendererHash,
.flags = .{
.setSourceGeometry = false,
},
},
} });
self._displayLayout.items[node.index] = .{
.baseSize = n.baseSize,
.pos = dlb.resolvedPos.add(Vector2f.Ones),
.size = dlb.resolvedSize.add(Vector2f.Ones),
.childLayoutOffsets = .{},
};
},
.Button => {
try NodeProperty_Button.addToDrawList(dlb);
},
.TextEntry => {
try NodeProperty_TextEntry.addToDrawList(dlb);
},
.Slot => {},
}
}
if (self.drawDebug) {
try self.addDebugInfo(drawList);
}
for (0..drawList.items.len) |i| {
switch (drawList.items[i].primitive) {
.Text => |*text| {
text.text.utf8 = try core.dupeString(stringArena.allocator(), text.text.utf8);
},
.Rect => {},
}
}
}
fn addDebugInfo(self: @This(), drawList: *DrawList) !void {
const defaultHeight = 16;
const sizePerLine: f32 = defaultHeight + 2;
const yOffsetPerLine: f32 = defaultHeight + 1;
var yOffset: f32 = sizePerLine;
const width = defaultHeight / 2 * 120;
const fontHash = self.fontCache.defaultMonoFont.atlas.rendererHash;
try self.mousePick.addMousePickInfo(&self, drawList);
try drawList.append(.{
.node = .{},
.primitive = .{
.Rect = .{
.tl = .{ .x = 30 - 5, .y = yOffset - 5 },
.size = .{ .x = width + 5, .y = sizePerLine * @as(f32, @floatFromInt(self.debugTextCount + 2)) },
.borderColor = Color.fromRGBA(0x444444ee),
.backgroundColor = Color.fromRGBA2(0.0, 0.0, 0.0, 0.94),
},
},
});
// try drawList.append(.{
// .node = .{},
// .primitive = .{
// .Text = .{
// .text = LocText.fromUtf8("Papyrus Debug:"),
// .tl = .{ .x = 30, .y = yOffset },
// .size = .{ .x = width, .y = 30 },
// .renderMode = .NoControl,
// //.color = BurnStyle.Highlight3,
// .color = Color.Yellow,
// .textSize = defaultHeight,
// .rendererHash = fontHash,
// .flags = .{
// .setSourceGeometry = false,
// },
// },
// },
// });
// yOffset += yOffsetPerLine;
for (self.debugText.items, 0..) |textData, i| {
if (i >= self.debugTextCount) {
break;
}
try drawList.append(.{
.node = .{},
.primitive = .{
.Text = .{
.text = LocText.fromUtf8Z(textData),
.tl = .{ .x = 30, .y = yOffset },
.size = .{ .x = width, .y = 30 },
.renderMode = .NoControl,
.color = Color.Yellow,
.textSize = defaultHeight,
.rendererHash = fontHash,
.flags = .{
.setSourceGeometry = false,
},
},
},
});
yOffset += yOffsetPerLine + (yOffsetPerLine / 4);
}
}
pub fn printTree(self: @This(), root: NodeHandle) void {
std.debug.print("\n ==== tree ==== \n", .{});
self.printTreeInner(root, 0);
std.debug.print(" ==== /tree ==== \n\n", .{});
}
fn printTreeInner(self: @This(), root: NodeHandle, indentLevel: u32) void {
var i: u32 = 0;
while (i < indentLevel) : (i += 1) {
std.debug.print(" ", .{});
}
const node = self.getRead(root);
const text = node.text.getRead();
if (text.len > 256) {
std.debug.print("> {d}: {s}...\n", .{ root.index, text[0..128] });
} else {
std.debug.print("> {d}: {s}\n", .{ root.index, text });
}
var next = node.child;
while (next.index != 0) {
self.printTreeInner(next, indentLevel + 1);
next = self.getRead(next).*.next;
}
}
pub fn writeTree(self: @This(), root: NodeHandle, outFile: []const u8) !void {
var log = try FileLog.init(self.allocator, outFile);
defer log.deinit();
try log.write("digraph G {{\n", .{});
try self.writeTreeInner(root, &log);
try log.write("}}\n", .{});
try log.writeOut();
}
fn writeTreeInner(self: @This(), root: NodeHandle, log: *FileLog) !void {
var node = self.getRead(root);
try log.write(" {s}->{s}", .{ self.getRead(node.parent).text.getRead(), node.text.getRead() });
var next = node.child;
while (next.index != 0) {
try self.writeTreeInner(next, log);
next = self.getRead(next).*.next;
}
}
// Sets the current cursor location
pub fn setCursorLocation(self: *@This(), position: Vector2f) void {
self.currentCursorPosition = position;
}
};
pub const Module = core.ModuleDescription{
.name = "papyrus",
.enabledByDefault = true,
};