const std = @import("std"); const math = @import("math.zig"); const config = @import("config.zig"); const c = @import("c.zig").c; const Vec2f = math.Vec2f; const Config = config.Config; pub const Camera = struct { position: Vec2f = .{}, velocity: Vec2f = .{}, scale: f32 = 1.0, delta_scale: f32 = 0.0, scale_pivot: Vec2f = .{}, }; pub const Mouse = struct { curr: Vec2f = .{}, prev: Vec2f = .{}, drag: bool = false, }; pub const Flashlight = struct { enabled: bool = false, shadow: f32 = 0.0, radius: f32 = 0.0, delta_radius: f32 = 0.0, }; pub const State = struct { camera: Camera = .{}, mouse: Mouse = .{}, flashlight: Flashlight = .{}, dt: f32 = 0.0, running: bool = true, mirror: bool = false, }; pub const App = struct { config: Config = Config.default(), state: State = .{}, config_path: ?[]const u8 = null, allocator: std.mem.Allocator, pub fn init(alloc: std.mem.Allocator, cfg_path: ?[]const u8) !App { var app = App{ .allocator = alloc, .config = Config.default(), }; if (cfg_path) |p| { app.config_path = try alloc.dupe(u8, p); app.config.loadFromFile(p); } app.state.flashlight.radius = app.config.initial_radius; return app; } pub fn deinit(self: *App) void { if (self.config_path) |p| { self.allocator.free(p); } } pub fn cameraUpdate(self: *App, ws: Vec2f) void { const cfg = &self.config; const cam = &self.state.camera; const m = &self.state.mouse; const dt = self.state.dt; if (@abs(cam.delta_scale) > cfg.scale_change_threshold) { const half = ws.mul(0.5); const sub = cam.scale_pivot.sub(half); const p0 = sub.div(cam.scale); cam.scale += cam.delta_scale * dt; if (cam.scale < cfg.min_scale) cam.scale = cfg.min_scale; const p1 = sub.div(cam.scale); cam.position = cam.position.add(p0.sub(p1)); cam.delta_scale -= cam.delta_scale * dt * cfg.scale_friction; } if (!m.drag and cam.velocity.length() > cfg.velocity_threshold) { cam.position = cam.position.add(cam.velocity.mul(dt)); cam.velocity = cam.velocity.sub(cam.velocity.mul(dt * cfg.drag_friction)); } } pub fn flashlightUpdate(self: *App) void { const fl = &self.state.flashlight; const dt = self.state.dt; const cfg = &self.config; fl.shadow = if (fl.enabled) @min(fl.shadow + cfg.fade_speed * dt, cfg.max_shadow_opacity) else @max(fl.shadow - cfg.fade_speed * dt, 0.0); if (@abs(fl.delta_radius) > cfg.radius_change_threshold) { fl.radius = @max(0.0, fl.radius + fl.delta_radius * dt); fl.delta_radius -= fl.delta_radius * cfg.radius_damping * dt; } } fn worldPosition(camera: *Camera, pos: Vec2f) Vec2f { return pos.div(camera.scale); } pub fn processEvents(self: *App, x11: anytype) void { var ev: c.XEvent = undefined; while (c.XPending(x11.display) != 0) { _ = c.XNextEvent(x11.display, &ev); switch (ev.type) { c.KeyPress => self.handleKeypress(&ev.xkey), c.MotionNotify => self.handleMousemove(&ev.xmotion, x11.refresh_rate), c.ButtonPress => self.handleButtonpress(&ev.xbutton), c.ButtonRelease => self.handleButtonrelease(&ev.xbutton), c.ClientMessage => { if (@as(c.Atom, @bitCast(ev.xclient.data.l[0])) == x11.wm_delete_window) self.state.running = false; }, else => {}, } } } fn handleKeypress(self: *App, ke: *c.XKeyEvent) void { const key = c.XLookupKeysym(ke, 0); if (key == self.config.key_escape or key == c.XK_q) self.state.running = false; if (key == c.XK_r) { if (self.config_path) |p| { self.config.loadFromFile(p); } } if (key == self.config.key_flashlight) self.state.flashlight.enabled = !self.state.flashlight.enabled; if (key == self.config.key_reset) { self.state.camera = .{ .scale = 1.0 }; self.state.flashlight.shadow = 0.0; self.state.flashlight.radius = self.config.initial_radius; self.state.flashlight.delta_radius = 0.0; } if (key == self.config.key_mirror) self.state.mirror = !self.state.mirror; if (key == self.config.key_zoom_in) { self.state.camera.delta_scale += self.config.scroll_speed; self.state.camera.scale_pivot = self.state.mouse.curr; } if (key == self.config.key_zoom_out) { self.state.camera.delta_scale -= self.config.scroll_speed; self.state.camera.scale_pivot = self.state.mouse.curr; } } fn handleMousemove(self: *App, motion: *c.XMotionEvent, rr: i32) void { self.state.mouse.curr = .{ .x = @floatFromInt(motion.x), .y = @floatFromInt(motion.y) }; if (self.state.mouse.drag) { const prev = worldPosition(&self.state.camera, self.state.mouse.prev); const cur = worldPosition(&self.state.camera, self.state.mouse.curr); self.state.camera.position = self.state.camera.position.add(prev.sub(cur)); self.state.camera.velocity = prev.sub(cur).mul(@floatFromInt(rr)); } self.state.mouse.prev = self.state.mouse.curr; } fn handleButtonpress(self: *App, be: *c.XButtonEvent) void { const ctrl_pressed = (be.state & self.config.modifier_flashlight) != 0; if (be.button == self.config.button_drag) { self.state.mouse.prev = self.state.mouse.curr; self.state.mouse.drag = true; self.state.camera.velocity = .{}; } else if (be.button == self.config.button_zoom_in) { if (ctrl_pressed and self.state.flashlight.enabled) { self.state.flashlight.delta_radius += self.config.initial_delta_radius; } else { self.state.camera.delta_scale += self.config.scroll_speed; self.state.camera.scale_pivot = self.state.mouse.curr; } } else if (be.button == self.config.button_zoom_out) { if (ctrl_pressed and self.state.flashlight.enabled) { self.state.flashlight.delta_radius -= self.config.initial_delta_radius; } else { self.state.camera.delta_scale -= self.config.scroll_speed; self.state.camera.scale_pivot = self.state.mouse.curr; } } } fn handleButtonrelease(self: *App, be: *c.XButtonEvent) void { if (be.button == self.config.button_drag) self.state.mouse.drag = false; } };