1
0
Fork 0
zenolith-sdl2/src/Sdl2Platform.zig
mcmrcs@proton.me 563e01c2f2 feat: implemented create and update texture (#2)
Reviewed-on: https://git.mzte.de/zenolith/zenolith-sdl2/pulls/2
Co-authored-by: mcmrcs@proton.me <marciusdasilva51@gmail.com>
Co-committed-by: mcmrcs@proton.me <marciusdasilva51@gmail.com>
2024-03-18 23:16:48 +01:00

419 lines
15 KiB
Zig

const std = @import("std");
const zenolith = @import("zenolith");
const ffi = @import("ffi.zig");
const c = ffi.c;
const util = @import("util.zig");
const log = std.log.scoped(.zenolith_sdl2);
const Sdl2Font = @import("Sdl2Font.zig");
const Sdl2Painter = @import("Sdl2Painter.zig");
const Sdl2Texture = @import("Sdl2Texture.zig");
// These decls are public so that zenolith includes them in it's statspatch types.
pub const Font = Sdl2Font;
pub const Texture = Sdl2Texture;
alloc: std.mem.Allocator,
window: *c.SDL_Window,
renderer: *c.SDL_Renderer,
freetype: c.FT_Library,
mouse_pos: ?zenolith.layout.Position,
initial_run: bool,
const Sdl2Platform = @This();
pub const InitOptions = struct {
alloc: std.mem.Allocator,
/// The title of the window the platform will create.
window_title: [*:0]const u8 = "Zenolith SDL2",
/// An optional initial window position.
window_position: ?[2]c_int = null,
/// Initial window size
window_size: [2]c_int = .{ 800, 600 },
};
pub const InitializeError = error{
/// An error has occured trying to initialize the SDL2 library.
InitializeSDL2,
/// An error has occured trying to initialize the freetype2 library.
InitializeFreetype,
/// SDL2 failed to create a window.
CreateWindow,
/// SDL2 failed to create a renderer.
CreateRenderer,
};
/// Create a new SDL2 Platform. Initializes the SDL2 and FreeType libraries and creates a
/// window. Do not create multiple Sdl2Platforms at once!
// TODO: Window API
pub fn init(options: InitOptions) InitializeError!Sdl2Platform {
if (c.SDL_Init(c.SDL_INIT_VIDEO) != 0) return error.InitializeSDL2;
errdefer c.SDL_Quit();
const window_pos = options.window_position orelse [1]c_int{c.SDL_WINDOWPOS_UNDEFINED} ** 2;
const window = c.SDL_CreateWindow(
options.window_title,
window_pos[0],
window_pos[1],
options.window_size[0],
options.window_size[1],
c.SDL_WINDOW_SHOWN | c.SDL_WINDOW_RESIZABLE, // TODO: add API to change this
) orelse return error.CreateWindow;
errdefer c.SDL_DestroyWindow(window);
const renderer = c.SDL_CreateRenderer(
window,
-1,
c.SDL_RENDERER_ACCELERATED,
) orelse return error.CreateRenderer;
errdefer c.SDL_DestroyRenderer(renderer);
var freetype: c.FT_Library = undefined;
if (c.FT_Init_FreeType(&freetype) != 0) return error.InitializeFreetype;
errdefer _ = c.FT_Done_FreeType(freetype);
return .{
.alloc = options.alloc,
.window = window,
.renderer = renderer,
.freetype = freetype,
.mouse_pos = null,
.initial_run = true,
};
}
/// Runs the event loop until the application exists.
pub fn run(
self: *Sdl2Platform,
root: *zenolith.widget.Widget,
) anyerror!void {
// Initial layout pass before we get a resize event.
if (self.initial_run) {
try self.relayoutRoot(root);
self.initial_run = false;
}
var last_time = std.time.nanoTimestamp();
while (true) {
c.SDL_PumpEvents();
// Wait until events are available.
if (c.SDL_WaitEvent(null) != 1) return error.GetEvents;
// Get all pending events.
var ev_buf: [64]c.SDL_Event = undefined;
const peep_ret = c.SDL_PeepEvents(
&ev_buf,
ev_buf.len,
c.SDL_GETEVENT,
c.SDL_FIRSTEVENT,
c.SDL_LASTEVENT,
);
if (peep_ret < 0) return error.GetEvents;
const events = ev_buf[0..@intCast(peep_ret)];
// This code is responsible for building one KeyInput event out of consecutive
// SDL_KEYUP, SDL_KEYDOWN and SDL_TEXTINPUT events.
var text_input_buf: [1024]u8 = undefined;
var text_input_fbs = std.io.fixedBufferStream(&text_input_buf);
var cur_key_event = zenolith.treevent.KeyInput{
.action = .press,
.key = null,
};
for (events) |ev| {
switch (ev.type) {
// quit event - exit
c.SDL_QUIT => return,
c.SDL_WINDOWEVENT => switch (ev.window.event) {
// window resized - redo tree layout
c.SDL_WINDOWEVENT_RESIZED => {
const size = zenolith.layout.Size{
.width = @intCast(ev.window.data1),
.height = @intCast(ev.window.data2),
};
try zenolith.treevent.fire(root, zenolith.treevent.LayoutSize{
.final = true,
.constraints = .{
.min = zenolith.layout.Size.zero,
.max = size,
},
});
try zenolith.treevent.fire(root, zenolith.treevent.LayoutPosition{
.position = zenolith.layout.Position.zero,
});
},
else => {},
},
c.SDL_MOUSEMOTION => {
const mouse_pos = zenolith.layout.Position{
.x = @intCast(ev.motion.x),
.y = @intCast(ev.motion.y),
};
self.mouse_pos = mouse_pos;
try zenolith.treevent.fire(root, zenolith.treevent.MouseMove{
.pos = mouse_pos,
.dx = ev.motion.xrel,
.dy = ev.motion.yrel,
});
},
c.SDL_MOUSEBUTTONDOWN, c.SDL_MOUSEBUTTONUP => {
const button: ?zenolith.treevent.Click.MouseButton = switch (ev.button.button) {
c.SDL_BUTTON_LEFT => .left,
c.SDL_BUTTON_MIDDLE => .middle,
c.SDL_BUTTON_RIGHT => .right,
else => null,
};
const actions: []const zenolith.treevent.Click.Action = switch (ev.type) {
c.SDL_MOUSEBUTTONDOWN => &.{ .down, .click },
c.SDL_MOUSEBUTTONUP => &.{.up},
else => unreachable,
};
if (button) |but| {
for (actions) |act| {
try zenolith.treevent.fire(root, zenolith.treevent.Click{
.pos = .{
.x = @intCast(ev.button.x),
.y = @intCast(ev.button.y),
},
.button = but,
.action = act,
});
}
}
},
c.SDL_MOUSEWHEEL => if (self.mouse_pos) |mp| {
const btn: zenolith.treevent.Click.MouseButton = if (ev.wheel.y > 0)
.scroll_up
else if (ev.wheel.y < 0)
.scroll_down
else if (ev.wheel.x > 0)
.scroll_right
else if (ev.wheel.x < 0)
.scroll_left
else
unreachable;
try zenolith.treevent.fire(root, zenolith.treevent.Click{
.pos = mp,
.button = btn,
.action = .click,
});
},
c.SDL_KEYDOWN, c.SDL_KEYUP => {
const m = ev.key.keysym.mod;
const mods = zenolith.key.Modifiers{
.shift = m & c.KMOD_SHIFT != 0,
.ctrl = m & c.KMOD_CTRL != 0,
.alt = m & c.KMOD_ALT != 0,
.meta = m & c.KMOD_GUI != 0,
.mode = m & c.KMOD_MODE != 0,
};
const key = util.convertKey(ev.key.keysym.sym);
const phys = util.convertScancode(ev.key.keysym.scancode);
switch (ev.type) {
c.SDL_KEYDOWN => {
// We know there's a text input or key press event that hasn't been fired yet,
// separate from this one if there's either a key set or text written to the input buffer.
if (cur_key_event.key != null or text_input_fbs.pos != 0) {
cur_key_event.text = text_input_fbs.getWritten();
try zenolith.treevent.fire(root, cur_key_event);
text_input_fbs.reset();
}
if (ev.key.repeat == 0) {
try zenolith.treevent.fire(root, zenolith.treevent.KeyInput{
.action = .down,
.key = key,
.physical = phys,
.modifiers = mods,
});
}
cur_key_event = .{
.action = .press,
.key = key,
.physical = phys,
.modifiers = mods,
.repeat = ev.key.repeat != 0,
};
},
c.SDL_KEYUP => {
try zenolith.treevent.fire(root, zenolith.treevent.KeyInput{
.action = .up,
.key = key,
.physical = phys,
.modifiers = mods,
});
},
else => unreachable,
}
},
c.SDL_TEXTINPUT => {
try text_input_fbs.writer().writeAll(std.mem.sliceTo(&ev.text.text, 0));
},
else => {},
}
}
// Fire the text input treevent when applicable
if (cur_key_event.key != null or text_input_fbs.pos != 0) {
cur_key_event.text = text_input_fbs.getWritten();
try zenolith.treevent.fire(root, cur_key_event);
}
// do render pass after events
// TODO: lazify
if (c.SDL_SetRenderDrawColor(self.renderer, 0, 0, 0, 0xff) != 0) return error.Render;
if (c.SDL_RenderClear(self.renderer) != 0) return error.Render;
var painter = zenolith.painter.Painter.create(
Sdl2Painter{ .renderer = self.renderer },
zenolith.painter.PainterData.init(root.data.allocator),
);
defer painter.data.deinit();
const current_time = std.time.nanoTimestamp();
try zenolith.treevent.fire(root, zenolith.treevent.Draw{
.painter = &painter,
.dt = @intCast(current_time - last_time),
});
last_time = current_time;
c.SDL_RenderPresent(self.renderer);
}
}
/// Quits a running application by submitting a quit event to the event queue,
/// making the application exit after all queued events were processed.
pub fn quit(self: *Sdl2Platform) !void {
_ = self; // Pretend this isn't global state.
var ev = c.SDL_Event{ .quit = .{ .type = c.SDL_QUIT } };
if (c.SDL_PushEvent(&ev) < 0) return error.PushEvent;
}
pub fn deinit(self: Sdl2Platform) void {
_ = c.FT_Done_FreeType(self.freetype);
c.SDL_DestroyRenderer(self.renderer);
c.SDL_DestroyWindow(self.window);
c.SDL_Quit();
}
pub const CreateFontOptions = struct {
/// Source data to open the font from.
source: union(enum) {
/// Read the font at a given file path.
path: [*:0]const u8,
/// Use the data of the slice as font. Useful with @embedFile.
/// The data must remain alive until .deinit() is called on the returned font!
data: []const u8,
},
/// This is FreeType's infamous face_index parameter. You should either consider reading the
/// short novel of historical design mistakes that is it's documentation here:
/// https://freetype.org/freetype2/docs/reference/ft2-face_creation.html#ft_open_face
/// ...or leave it as 0.
face_index: c_long = 0,
/// Size of the font atlas texture. Increase this if you needs to render lots of glyphs.
/// The bin packing algorithm performs best with tall rather than wide atlases.
atlas_size: zenolith.layout.Size = .{ .width = 512, .height = 1024 },
};
pub const CreateFontError = ffi.FreeTypeError || error{ CreateTexture, SetBlendMode };
pub fn createFont(self: Sdl2Platform, opts: CreateFontOptions) CreateFontError!Sdl2Font {
var face: c.FT_Face = undefined;
switch (opts.source) {
.path => |p| try ffi.handleFTError(c.FT_New_Face(
self.freetype,
p,
opts.face_index,
&face,
)),
.data => |d| try ffi.handleFTError(c.FT_New_Memory_Face(
self.freetype,
d.ptr,
@intCast(d.len),
opts.face_index,
&face,
)),
}
errdefer _ = c.FT_Done_Face(face);
const atlas = try self.createTexture(.{
.size = opts.atlas_size,
.pixel_format = .RGBA8888,
.pixel_access = .static,
});
try atlas.setBlendMode(.blend);
return .{
.face = face,
.atlas = atlas,
.renderer = self.renderer,
.glyphs = std.AutoArrayHashMap(Sdl2Font.GlyphProperties, Sdl2Font.AtlasGlyph).init(self.alloc),
.pixel_buf = std.ArrayList(u8).init(self.alloc),
};
}
pub fn relayoutRoot(self: *Sdl2Platform, root: *zenolith.widget.Widget) !void {
var width: c_int = 0;
var height: c_int = 0;
c.SDL_GetWindowSize(self.window, &width, &height);
try zenolith.treevent.fire(root, zenolith.treevent.LayoutSize{
.final = true,
.constraints = .{
.min = .{ .width = 0, .height = 0 },
.max = .{ .width = @intCast(width), .height = @intCast(height) },
},
});
try zenolith.treevent.fire(root, zenolith.treevent.LayoutPosition{
.position = .{ .x = 0, .y = 0 },
});
}
const CreateTextureOptions = struct {
size: zenolith.layout.Size,
pixel_format: Sdl2Texture.PixelFormat,
pixel_access: Sdl2Texture.PixelAccess = .static,
};
pub fn createTexture(self: Sdl2Platform, options: CreateTextureOptions) !Texture {
const texture = c.SDL_CreateTexture(
self.renderer,
@intFromEnum(options.pixel_format),
@intFromEnum(options.pixel_access),
options.size.width,
options.size.height,
) orelse return error.CreateTexture;
return Texture{
.tex = texture,
};
}