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
8 changes: 7 additions & 1 deletion app/settings/Settings.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ const build_opts = @import("build_opts");

const Settings = @This();

pub const default_theme = "Fizzy Dark";
/// The theme a fresh settings store starts with: the app's (`-Dapp-default-theme`,
/// `build/app.zig`), or fizzy's. It names one of the themes the editor registers (fizzy's, the
/// user's, or the app's own at `-Dapp-theme`); a name nothing registered logs and falls back.
pub const default_theme = if (@hasDecl(build_opts, "app_default_theme") and build_opts.app_default_theme.len > 0)
build_opts.app_default_theme
else
"Fizzy Dark";

/// Duration after the last edit before autosave runs (during normal operation).
pub const autosave_timeout_ns: i128 = 500 * 1_000_000;
Expand Down
27 changes: 27 additions & 0 deletions build/app.zig
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ pub const Config = struct {
app_repo_url_set: bool,
app_issues_url: []const u8,
app_layout_path: ?std.Build.LazyPath,
app_theme_path: ?std.Build.LazyPath,
web_storage: []const u8,
static_workbench: bool,
static_text: bool,
Expand Down Expand Up @@ -526,7 +527,23 @@ pub fn readConfig(b: *std.Build, target: std.Build.ResolvedTarget, opts: Options
"app-layout",
"App-owned layout file (pub fn layout(?*anyopaque, *Layout))",
);
// A consumer that wants its own themes passes `-Dapp-theme=` (a LazyPath to
// `pub fn add(gpa, themes: *std.ArrayList(dvui.Theme)) !void`), which the editor appends
// beside the user's, and `-Dapp-default-theme=` names the theme a fresh settings store
// starts with. Fizzy itself ships its own three and starts on `Fizzy Dark`.
const app_theme_path = b.option(
std.Build.LazyPath,
"app-theme",
"App-owned themes file (pub fn add(gpa, *std.ArrayList(dvui.Theme)) !void)",
);
const app_default_theme = b.option(
[]const u8,
"app-default-theme",
"The theme a fresh settings store starts with (default: Fizzy Dark)",
);
build_opts.addOption(bool, "has_app_layout", app_layout_path != null);
build_opts.addOption(bool, "has_app_theme", app_theme_path != null);
build_opts.addOption([]const u8, "app_default_theme", app_default_theme orelse "");
// The frame of fizzy's own shape an app keeps (`src/editor/Chrome.zig`); the defaults are
// fizzy's own.
const chrome_sidebar_edge = b.option(
Expand Down Expand Up @@ -631,6 +648,7 @@ pub fn readConfig(b: *std.Build, target: std.Build.ResolvedTarget, opts: Options
.app_repo_url_set = app_repo_url_set,
.app_issues_url = app_issues_url,
.app_layout_path = app_layout_path,
.app_theme_path = app_theme_path,
.web_storage = web_storage,
.static_workbench = static_workbench,
.static_text = static_text,
Expand Down Expand Up @@ -696,6 +714,7 @@ pub fn construct(
assets_module,
app_icon_module,
cfg.app_layout_path,
cfg.app_theme_path,
try @import("sdk.zig").pluginsFor(b, opts.app_plugins, .web),
opts.web_plugin_deps,
opts.web_plugin_dirs,
Expand Down Expand Up @@ -784,6 +803,7 @@ fn native(
const app_repo_url = cfg.app_repo_url;
const app_repo_url_fallback = cfg.app_repo_url_fallback;
const app_layout_path = cfg.app_layout_path;
const app_theme_path = cfg.app_theme_path;
const static_workbench = cfg.static_workbench;
const static_text = cfg.static_text;
const static_image = cfg.static_image;
Expand All @@ -808,6 +828,7 @@ fn native(
velopack_enabled,
app_name,
app_layout_path,
app_theme_path,
app_plugins,
dynamic_plugins,
);
Expand Down Expand Up @@ -857,6 +878,7 @@ fn native(
true,
app_name,
app_layout_path,
app_theme_path,
app_plugins,
dynamic_plugins,
);
Expand Down Expand Up @@ -1535,6 +1557,11 @@ fn addTests(
.optimize = optimize,
.root_source_file = b.path("src/fizzy.zig"),
});
// The app's themes module, when the build was given one (`-Dapp-theme`): the editor's
// comptime `has_app_theme` branch imports it.
if (cfg.app_theme_path) |path| {
fizzy_test_module.addAnonymousImport("app_theme", .{ .root_source_file = path });
}
fizzy_test_module.addImport("dvui", dvui_testing_dep.module("dvui_testing"));
fizzy_test_module.addImport("backend", dvui_testing_dep.module("testing"));
// The testing backend has no viewports (`backend_web.viewports`).
Expand Down
4 changes: 4 additions & 0 deletions build/exe.zig
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@ pub fn addFizzyExecutableForTarget(
/// `Host.layout_ctx`. The file imports `dvui`, `app`, `core`, and `fizzy_sdk` — not
/// `fizzy` / `Editor` — so the module graph does not cycle.
app_layout: ?std.Build.LazyPath,
/// Consumer-owned themes file (`-Dapp-theme`). When set, Editor appends its themes beside
/// the user's. The file imports `dvui`.
app_theme: ?std.Build.LazyPath,
/// Every plugin the application links, in registration order: fizzy's own
/// (`fizzy.plugins`, built here) and the application's, each a static module from a plugin
/// package (`plugin_dep.module("plugin")`). Listed in the generated `bundled_plugins` module
Expand Down Expand Up @@ -458,6 +461,7 @@ pub fn addFizzyExecutableForTarget(
sdk_module,
exe.root_module,
);
sdk.wireAppThemes(b, resolved_target, optimize, app_theme, dvui_mod, exe.root_module);

var dylibs: std.ArrayList(Dylib) = .empty;
const workbench_dylib: ?*std.Build.Step.Compile = if (dynamic_plugins and
Expand Down
22 changes: 22 additions & 0 deletions build/sdk.zig
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,28 @@ pub fn wireAppLayout(
}
}

/// Wire an app-owned themes module against this target's framework modules: the editor appends
/// its themes beside the user's when the build was given one (`-Dapp-theme`, see
/// `build/app.zig`).
pub fn wireAppThemes(
b: *std.Build,
target: std.Build.ResolvedTarget,
optimize: std.lang.OptimizeMode,
app_theme: ?std.Build.LazyPath,
dvui: *std.Build.Module,
consumer: *std.Build.Module,
) void {
if (app_theme) |path| {
const mod = b.createModule(.{
.target = target,
.optimize = optimize,
.root_source_file = path,
});
mod.addImport("dvui", dvui);
consumer.addImport("app_theme", mod);
}
}

/// The whole of a `.zon` shape's generated module: import the file as a `Shape` — which is where
/// it is type-checked, at comptime, against the field names and enums `Shape` declares —
/// and hand it to the same walk a hand-written shape's calls would have performed itself.
Expand Down
2 changes: 2 additions & 0 deletions build/web.zig
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ pub fn build(
/// The window's icon (`app.Branding.png`); `src/Entry.zig` imports it.
app_icon_module: *std.Build.Module,
app_layout: ?std.Build.LazyPath,
app_theme: ?std.Build.LazyPath,
app_plugins: []const sdk.BundledPlugin,
web_plugin_deps: []const []const u8,
web_plugin_dirs: []const @import("app.zig").WebPluginDir,
Expand Down Expand Up @@ -352,6 +353,7 @@ pub fn build(
sdk_module_web,
web_exe.root_module,
);
sdk.wireAppThemes(b, web_target, optimize, app_theme, dvui_web, web_exe.root_module);

// Cache-buster: stamps a 64-char hash into the index.html / fizzy.js placeholders so
// the browser picks up new wasm builds without manual hard-reloads. Re-implements
Expand Down
75 changes: 75 additions & 0 deletions sdk/src/Host.zig
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ pub const OpenAction = @import("menus.zig").OpenAction;
pub const accounts = @import("accounts.zig");
pub const Command = @import("Command.zig");
pub const Demo = @import("Demo.zig");

/// A theme a plugin registered (`Host.registerTheme`): the theme by value, its name the host's
/// own copy, and who registered it, so the themes go with the plugin when it retires.
pub const HostTheme = struct {
owner: ?*Plugin = null,
name: []u8,
theme: dvui.Theme,
};
pub const Claim = @import("Claim.zig");
pub const Step = @import("Step.zig");
pub const Author = @import("Author.zig");
Expand Down Expand Up @@ -265,6 +273,8 @@ open_actions: std.ArrayList(OpenAction) = .empty,
demos: std.ArrayList(Demo) = .empty,
/// Moves whenever `demos` changes, so the app lists them again only then.
demos_generation: u32 = 0,
/// Plugins' themes (`registerTheme`): the theme by value, and its name the host's own copy.
themes: std.ArrayList(HostTheme) = .empty,
/// Who the user is signed in as, per service (`accounts.Provider`). The host draws the disc.
account_providers: std.ArrayList(accounts.Provider) = .empty,
/// Pure-data menu leaf items the native (macOS NSMenu) menu builder consumes; see
Expand Down Expand Up @@ -390,6 +400,8 @@ pub fn deinit(self: *Host) void {
self.open_actions.deinit(self.allocator);
for (self.demos.items) |d| self.freeDemo(d);
self.demos.deinit(self.allocator);
for (self.themes.items) |t| self.allocator.free(t.name);
self.themes.deinit(self.allocator);
self.account_providers.deinit(self.allocator);
self.native_menu_items.deinit(self.allocator);
self.commands.deinit(self.allocator);
Expand Down Expand Up @@ -1078,6 +1090,7 @@ fn dropRegistrations(self: *Host, plugin: *Plugin) void {
removeOwned(MenuSectionContribution, &self.menu_sections, plugin);
removeOwned(OpenAction, &self.open_actions, plugin);
removeOwned(RailItemContribution, &self.rail_items, plugin);
self.removeThemesOf(plugin);
self.removeDemosOf(plugin);
removeOwned(accounts.Provider, &self.account_providers, plugin);
removeOwned(NativeMenuItem, &self.native_menu_items, plugin);
Expand Down Expand Up @@ -1648,6 +1661,36 @@ fn freeDemo(self: *Host, d: Demo) void {
self.allocator.free(d.tape);
}

/// Register a theme the editor appends beside the user's and the app's — see
/// `Editor.themes`. Copied: only the theme's `name` is kept; its font families must outlive
/// the registration (literals, as `Font.find` takes). A theme of the same name replaces the
/// old entry in place, so a reloaded plugin updates its theme instead of duplicating it.
pub fn registerTheme(self: *Host, owner: ?*Plugin, theme: dvui.Theme) !void {
const name = try self.allocator.dupe(u8, theme.name);
errdefer self.allocator.free(name);
for (self.themes.items) |*t| {
if (std.mem.eql(u8, t.name, theme.name)) {
self.allocator.free(t.name);
t.* = .{ .owner = owner, .name = name, .theme = theme };
return;
}
}
try self.themes.append(self.allocator, .{ .owner = owner, .name = name, .theme = theme });
}

fn removeThemesOf(self: *Host, plugin: *Plugin) void {
var w: usize = 0;
for (self.themes.items) |t| {
if (t.owner == plugin) {
self.allocator.free(t.name);
continue;
}
self.themes.items[w] = t;
w += 1;
}
self.themes.items.len = w;
}

fn removeDemosOf(self: *Host, plugin: *Plugin) void {
var w: usize = 0;
for (self.demos.items) |d| {
Expand Down Expand Up @@ -3160,6 +3203,38 @@ test "pluginForExtension honors a user assignment and ignores a stale one" {
Fake.override = null;
}

test "registerTheme keeps its own copy, replaces by name, and a plugin's themes go with it" {
var host = Host.init(testing.allocator);
defer host.deinit();
const vtable = Plugin.VTable{};
var plugin = Plugin{ .state = undefined, .vtable = &vtable, .id = "guide", .display_name = "Guide" };
try host.registerPlugin(&plugin);

var name_buf = "Graphl Dark".*;
var theme = dvui.Theme.builtin.adwaita_dark;
theme.name = &name_buf;
try host.registerTheme(&plugin, theme);
// Nothing points into what the plugin handed over.
name_buf[0] = 'x';
try testing.expectEqualStrings("Graphl Dark", host.themes.items[0].name);
try testing.expect(host.themes.items[0].theme.dark);
try testing.expectEqual(@as(usize, 1), host.themes.items.len);

// Same name replaces in place rather than duplicating.
theme = dvui.Theme.builtin.adwaita_light;
theme.name = "Graphl Dark";
try host.registerTheme(&plugin, theme);
try testing.expectEqual(@as(usize, 1), host.themes.items.len);
try testing.expect(!host.themes.items[0].theme.dark);

theme.name = "Graphl Light";
try host.registerTheme(&plugin, theme);
try testing.expectEqual(@as(usize, 2), host.themes.items.len);

host.unregisterPlugin(&plugin);
try testing.expectEqual(@as(usize, 0), host.themes.items.len);
}

test "registerDemo keeps its own copy, refuses a name twice, and a plugin's demos go with it" {
var host = Host.init(testing.allocator);
defer host.deinit();
Expand Down
2 changes: 1 addition & 1 deletion sdk/src/version.zig
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ pub const sdk_version = @import("sdk_version").sdk_version;
/// why it is a single target/mode-invariant literal rather than a per-target table. Update this
/// value from the `@compileError` it triggers whenever it changes; leave `sdk_version` to the
/// next SDK release.
pub const recorded_sdk_shape_fingerprint: u64 = 0x2d68c9a0cdaef363;
pub const recorded_sdk_shape_fingerprint: u64 = 0x271ebd51f39b07ed;

comptime {
if (dylib.sdk_shape_fingerprint != recorded_sdk_shape_fingerprint) {
Expand Down
40 changes: 40 additions & 0 deletions src/editor/Editor.zig
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,15 @@ pub fn init(
};
}

// The app's own themes, contributed at `-Dapp-theme` (see `build/app.zig`). Before
// fizzy's own, so an app's themes sit beside the user's at the top of the picker.
if (comptime build_opts.has_app_theme) {
@import("app_theme").add(app.allocator, &editor.themes) catch |err| {
dvui.log.err("Failed to append the app's themes: {t}", .{err});
return error.FailedToAppendAppThemes;
};
}

editor.themes.append(app.allocator, fizzy_dark) catch {
dvui.log.err("Failed to append theme", .{});
return error.FailedToAppendTheme;
Expand Down Expand Up @@ -1737,6 +1746,8 @@ const WebPluginRequest = struct {
}
rebuildKeybinds(editor);
editor.rebuildExtensionOwnerCache();
editor.pullPluginContributions() catch |err|
dvui.log.err("web plugin '{s}': themes: {s}", .{ req.id, @errorName(err) });
if (takeWebFileTypePrompt(gpa, req.id)) editor.maybeShowFileTypeDialog(req.id) catch |err|
dvui.log.err("web plugin '{s}': file-type prompt: {s}", .{ req.id, @errorName(err) });
// Remembered here, not by the page when it linked the module: the page cannot know
Expand Down Expand Up @@ -2365,6 +2376,8 @@ pub fn postInit(editor: *Editor) !void {
// Settings so its icon sits directly above the cog in the sidebar rail.
try PluginStore.register(editor.pluginManager());

try editor.pullPluginContributions();

// Fizzy built-in: Settings (owner = null; not a plugin).
try editor.app.host.registerSurface(.{
.id = view_settings,
Expand Down Expand Up @@ -3627,6 +3640,33 @@ fn shapedTheme(editor: *const Editor, base: dvui.Theme) dvui.Theme {
return theme;
}

/// What plugins registered in `initPlugin` that the editor holds itself, pulled once they have
/// initialized (at startup, and when a web plugin loads), since the editor's own apply ran in
/// `init`, before any plugin existed: their themes. Re-applies the settings theme when any came.
fn pullPluginContributions(editor: *Editor) !void {
if ((try editor.importHostThemes()) > 0) try editor.applySettingsTheme();
}

/// Pull the themes plugins registered (`Host.registerTheme`) into `editor.themes`: an entry
/// of the same name is replaced in place, new ones appended. Returns how many it pulled, so the
/// caller knows whether to re-apply the settings theme.
pub fn importHostThemes(editor: *Editor) !usize {
var changed: usize = 0;
for (editor.app.host.themes.items) |ht| {
var replaced = false;
for (editor.themes.items) |*t| {
if (std.mem.eql(u8, t.name, ht.name)) {
t.* = ht.theme;
replaced = true;
break;
}
}
if (!replaced) try editor.themes.append(editor.app.gpa, ht.theme);
changed += 1;
}
return changed;
}

pub fn applySettingsTheme(editor: *Editor) !void {
const t = resolveSettingsTheme(editor);
if (!std.mem.eql(u8, editor.app.settings.theme, t.name)) {
Expand Down
Loading