Skip to content
Open
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
79 changes: 76 additions & 3 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,15 @@ pub fn buildBackend(

// Examples, must be compiled for wasm32
{
const standalone_export_symbol_names = &[_][]const u8{
"main",
"add_event",
"arena_u8",
"gpa_u8",
"gpa_free",
"new_font",
};

var wasm_dvui_opts = DvuiModuleOptions{
.b = b,
.target = b.resolveTargetQuery(.{
Expand Down Expand Up @@ -1024,15 +1033,29 @@ pub fn buildBackend(
});
web_mod_wasm.export_symbol_names = export_symbol_names;

const web_standalone_mod_wasm = b.createModule(.{
.root_source_file = b.path("src/backends/web.zig"),
});
web_standalone_mod_wasm.export_symbol_names = standalone_export_symbol_names;

const dvui_web_wasm = addDvuiModule("dvui_web_wasm", wasm_dvui_opts);
const dvui_web_standalone_wasm = addDvuiModule("dvui_web_standalone_wasm", wasm_dvui_opts);
linkBackend(dvui_web_wasm, web_mod_wasm);
const example_opts: ExampleOptions = .{
linkBackend(dvui_web_standalone_wasm, web_standalone_mod_wasm);

const app_example_opts: ExampleOptions = .{
.dvui_mod = dvui_web_wasm,
.backend_name = "web-backend",
.backend_mod = web_mod_wasm,
};
addWebExample("web-test", b.path("examples/web-test.zig"), example_opts, wasm_dvui_opts, web_serve_exe);
addWebExample("web-app", b.path("examples/app.zig"), example_opts, wasm_dvui_opts, web_serve_exe);
const standalone_example_opts: ExampleOptions = .{
.dvui_mod = dvui_web_standalone_wasm,
.backend_name = "web-backend",
.backend_mod = web_standalone_mod_wasm,
};
addWebExample("web-test", b.path("examples/web-test.zig"), app_example_opts, wasm_dvui_opts, web_serve_exe);
addWebExample("web-app", b.path("examples/app.zig"), app_example_opts, wasm_dvui_opts, web_serve_exe);
addWebStandaloneExample("web-standalone", b.path("examples/web-standalone.zig"), standalone_example_opts, wasm_dvui_opts);
}
},
.wio => {
Expand Down Expand Up @@ -1615,6 +1638,8 @@ fn addWebExample(
compile_step.dependOn(&b.addInstallFileWithDir(output, install_dir, "index.html").step);
const web_js = b.path("src/backends/web.js");
compile_step.dependOn(&b.addInstallFileWithDir(web_js, install_dir, "web.js").step);
const web_common = b.path("src/backends/web-common.js");
compile_step.dependOn(&b.addInstallFileWithDir(web_common, install_dir, "web-common.js").step);
b.addNamedLazyPath("web.js", web_js);
compile_step.dependOn(&install_wasm.step);
compile_step.dependOn(&install_noto.step);
Expand All @@ -1629,6 +1654,54 @@ fn addWebExample(
b.getInstallStep().dependOn(compile_step);
}

fn addWebStandaloneExample(
comptime name: []const u8,
file: std.Build.LazyPath,
example_opts: ExampleOptions,
opts: DvuiModuleOptions,
) void {
const b = opts.b;

const exeOptions: std.Build.ExecutableOptions = .{
.name = "web",
.root_module = b.createModule(.{
.root_source_file = file,
.target = opts.target,
.optimize = opts.optimize,
.link_libc = false,
.strip = if (opts.optimize == .ReleaseFast or opts.optimize == .ReleaseSmall) true else false,
}),
};
const web_exe = b.addExecutable(exeOptions);
web_exe.entry = .disabled;
web_exe.root_module.addImport("dvui", example_opts.dvui_mod);
web_exe.root_module.addImport(example_opts.backend_name, example_opts.backend_mod);

const web_check = b.addExecutable(exeOptions);
web_check.entry = .disabled;
web_check.root_module.addImport("dvui", example_opts.dvui_mod);
web_check.root_module.addImport(example_opts.backend_name, example_opts.backend_mod);
if (opts.check_step) |step| step.dependOn(&web_check.step);

const install_dir: std.Build.InstallDir = .{ .custom = "bin/" ++ name };

const install_wasm = b.addInstallArtifact(web_exe, .{
.dest_dir = .{ .override = install_dir },
});

const install_noto = b.addInstallFileWithDir(b.path("src/fonts/NotoSansKR-Regular.ttf"), install_dir, "NotoSansKR-Regular.ttf");

const compile_step = b.step(name, "Compile " ++ name ++ " (Web Worker standalone)");
compile_step.dependOn(&b.addInstallFileWithDir(b.path("src/backends/index-standalone.html"), install_dir, "index.html").step);
compile_step.dependOn(&b.addInstallFileWithDir(b.path("src/backends/web-standalone.js"), install_dir, "web-standalone.js").step);
compile_step.dependOn(&b.addInstallFileWithDir(b.path("src/backends/web-common.js"), install_dir, "web-common.js").step);
compile_step.dependOn(&b.addInstallFileWithDir(b.path("src/backends/web-worker.js"), install_dir, "web-worker.js").step);
compile_step.dependOn(&install_wasm.step);
compile_step.dependOn(&install_noto.step);

b.getInstallStep().dependOn(compile_step);
}

/// Given a lazy path to an svg file, e.g. `b.path('./src/image.svg')`
/// return a LazyPath containing the generated tvg bytes. You can use this
/// to make icons for DVUI apps from SVGs at build time
Expand Down
223 changes: 223 additions & 0 deletions examples/web-standalone.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
const std = @import("std");
const dvui = @import("dvui");
const WebBackend = @import("web-backend");

comptime {
std.debug.assert(@hasDecl(WebBackend, "WebBackend"));
}

const window_icon_png = @embedFile("zig-favicon.png");

const gpa: std.mem.Allocator = std.heap.wasm_allocator;

const vsync = true;
const show_demo = false;
var scale_val: f32 = 1.0;

var show_dialog_outside_frame: bool = false;
var g_initialized = false;
var g_interrupted = true;

pub const panic = WebBackend.panic;
pub const std_options: std.Options = .{
.logFn = WebBackend.logFn,
};

/// This example matches the SDL standalone structure:
/// - explicit backend and window setup
/// - explicit frame loop
/// - explicit event pump and wait
pub export fn main() void {
standaloneInit(null, 0) catch |err| {
std.log.err("Error during standalone init: {any}", .{err});
standaloneDeinit();
return;
};
defer standaloneDeinit();

main_loop: while (true) {
const wait_event_micros = (frameStep() catch |err| {
std.log.err("Error during frame step: {any}", .{err});
standaloneDeinit();
return;
}) orelse break :main_loop;
g_interrupted = try WebBackend.back.waitEventTimeout(wait_event_micros);
}
}

fn standaloneInit(platform_ptr: ?[*]const u8, platform_len: usize) !void {
_ = platform_ptr;
_ = platform_len;
if (g_initialized) return;

dvui.Examples.show_demo_window = show_demo;

WebBackend.back = try WebBackend.initWindow(.{
.allocator = gpa,
.size = .{ .w = 800.0, .h = 600.0 },
.min_size = .{ .w = 250.0, .h = 350.0 },
.vsync = vsync,
.title = "DVUI Web Standalone Example",
.icon = window_icon_png,
});

WebBackend.win = try dvui.Window.init(@src(), gpa, WebBackend.back.backend(), .{
.theme = switch (WebBackend.back.preferredColorScheme() orelse .light) {
.light => dvui.Theme.builtin.adwaita_light,
.dark => dvui.Theme.builtin.adwaita_dark,
},
});

WebBackend.win_ok = true;

g_interrupted = true;
g_initialized = true;
}

/// Execute one standalone frame.
/// Returns micros to wait before next frame, or null when app should close.
fn frameStep() !?u32 {
if (!g_initialized) return null;

var win_ref = &WebBackend.win;

const nstime = win_ref.beginWait(g_interrupted);
try win_ref.begin(nstime);

const keep_running = gui_frame();
if (!keep_running) return null;

const end_micros = try win_ref.end(.{});

if (show_dialog_outside_frame) {
show_dialog_outside_frame = false;
dvui.dialog(
@src(),
.{},
.{
.window = win_ref,
.modal = false,
.title = "Dialog from Outside",
.message = "This is a non modal dialog created outside win.begin()/win.end().",
},
);
}

return win_ref.waitTime(end_micros);
}

fn standaloneDeinit() void {
if (!g_initialized) return;
WebBackend.win_ok = false;
WebBackend.win.deinit();
WebBackend.back.deinit();
g_initialized = false;
}

// both dvui and backend drawing
// return false if user wants to exit the app
fn gui_frame() bool {
{
var hbox = dvui.box(@src(), .{ .dir = .horizontal }, .{ .style = .window, .background = true, .expand = .horizontal, .name = "main" });
defer hbox.deinit();

var m = dvui.menu(@src(), .horizontal, .{});
defer m.deinit();

if (dvui.menuItemLabel(@src(), "File", .{ .submenu = true }, .{})) |r| {
var fw = dvui.floatingMenu(@src(), .{ .from = r }, .{});
defer fw.deinit();

if (dvui.menuItemLabel(@src(), "Close Menu", .{}, .{ .expand = .horizontal }) != null) {
m.close();
}

if (dvui.menuItemLabel(@src(), "Exit", .{}, .{ .expand = .horizontal }) != null) {
return false;
}
}

if (dvui.menuItemLabel(@src(), "Edit", .{ .submenu = true }, .{})) |r| {
var fw = dvui.floatingMenu(@src(), .{ .from = r }, .{});
defer fw.deinit();
_ = dvui.menuItemLabel(@src(), "Dummy", .{}, .{ .expand = .horizontal });
_ = dvui.menuItemLabel(@src(), "Dummy Long", .{}, .{ .expand = .horizontal });
_ = dvui.menuItemLabel(@src(), "Dummy Super Long", .{}, .{ .expand = .horizontal });
}
}

var scroll = dvui.scrollArea(@src(), .{}, .{ .expand = .both });
defer scroll.deinit();

var tl = dvui.textLayout(@src(), .{}, .{ .expand = .horizontal, .font = .theme(.title) });
const lorem = "This example shows how to use dvui in a normal standalone application on the web.";
tl.addText(lorem, .{});
tl.deinit();

var tl2 = dvui.textLayout(@src(), .{}, .{ .expand = .horizontal });
tl2.addText(
\\DVUI
\\- paints the entire window
\\- can show floating windows and dialogs
\\- example menu at the top of the window
\\- rest of the window is a scroll area
\\
\\
, .{});
tl2.addText("Framerate is variable and adjusts as needed for input events and animations.\n\n", .{});
if (vsync) {
tl2.addText("Framerate is capped by vsync.\n", .{});
} else {
tl2.addText("Framerate is uncapped.\n", .{});
}
tl2.addText("\n", .{});
tl2.addText("Cursor is always being set by dvui.\n\n", .{});
if (dvui.useFreeType) {
tl2.addText("Fonts are being rendered by FreeType 2.", .{});
} else {
tl2.addText("Fonts are being rendered by stb_truetype.", .{});
}
tl2.deinit();

const label = if (dvui.Examples.show_demo_window) "Hide Demo Window" else "Show Demo Window";
if (dvui.button(@src(), label, .{}, .{})) {
dvui.Examples.show_demo_window = !dvui.Examples.show_demo_window;
}

if (dvui.button(@src(), "Debug Window", .{}, .{})) {
dvui.toggleDebugWindow();
}

{
var scaler = dvui.scale(@src(), .{ .scale = &scale_val }, .{ .expand = .horizontal });
defer scaler.deinit();

{
var hbox = dvui.box(@src(), .{ .dir = .horizontal }, .{});
defer hbox.deinit();

if (dvui.button(@src(), "Zoom In", .{}, .{})) {
scale_val = @round(dvui.themeGet().font_body.size * scale_val + 1.0) / dvui.themeGet().font_body.size;
}

if (dvui.button(@src(), "Zoom Out", .{}, .{})) {
scale_val = @round(dvui.themeGet().font_body.size * scale_val - 1.0) / dvui.themeGet().font_body.size;
}
}

dvui.labelNoFmt(@src(), "Backend-direct drawing demo is SDL-specific and omitted for web.", .{}, .{ .margin = .{ .x = 4 } });
}

if (dvui.button(@src(), "Show Dialog From\nOutside Frame", .{}, .{})) {
show_dialog_outside_frame = true;
}

dvui.Examples.demo(.full);

for (dvui.events()) |*e| {
if (e.evt == .window and e.evt.window.action == .close) return false;
if (e.evt == .app and e.evt.app.action == .quit) return false;
}

return true;
}
31 changes: 31 additions & 0 deletions src/backends/index-standalone.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<!doctype html>
<html lang="en" style="height: 100%;">
<head>
<meta charset="utf-8" />

<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />

<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<title>DVUI Web Standalone (Worker)</title>
</head>

<body style="height: 100%; margin: 0; padding: 0;">
<!-- need tabIndex for the canvas to receive keydown/keyup/beforeinput events -->
<canvas id="dvui-canvas" tabIndex="1" style="display: block; width: 100%; height: 100%; outline: none; caret-color: transparent;"></canvas>

<script type="module">
import { dvuiStandalone } from "./web-standalone.js";
dvuiStandalone("#dvui-canvas", "web.wasm", "web-worker.js");
</script>

<noscript>
<p>This application requires JavaScript and WebAssembly support.</p>
<p>Your server must also send the following headers for SharedArrayBuffer support:</p>
<pre>Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp</pre>
</noscript>
</body>
</html>
Loading