diff --git a/src/Library/demos/Custom Widget/code.rs b/src/Library/demos/Custom Widget/code.rs new file mode 100644 index 000000000..c086c6f7d --- /dev/null +++ b/src/Library/demos/Custom Widget/code.rs @@ -0,0 +1,66 @@ +use crate::workbench; +use gtk::{glib, subclass::prelude::*}; + +mod imp { + use super::*; + + #[derive(Debug, Default, gtk::CompositeTemplate)] + // The file will be provided by Workbench when the demo compiles, it just contains the template. + #[template(file = "workbench_template.ui")] + pub struct AwesomeButton {} + + #[glib::object_subclass] + impl ObjectSubclass for AwesomeButton { + const NAME: &'static str = "AwesomeButton"; + type Type = super::AwesomeButton; + type ParentType = gtk::Button; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + #[gtk::template_callbacks] + impl AwesomeButton { + #[template_callback] + fn onclicked(_button: >k::Button) { + println!("Clicked") + } + } + + impl ObjectImpl for AwesomeButton {} + impl WidgetImpl for AwesomeButton {} + impl ButtonImpl for AwesomeButton {} +} + +glib::wrapper! { + pub struct AwesomeButton(ObjectSubclass) @extends gtk::Widget, gtk::Button; +} + +impl AwesomeButton { + pub fn new() -> Self { + glib::Object::new() + } +} + +pub fn main() { + gtk::init().unwrap(); + + let container = gtk::ScrolledWindow::new(); + let flow_box = gtk::FlowBox::builder().hexpand(true).build(); + container.set_child(&flow_box); + + let mut widgets = Vec::with_capacity(100); + for _ in 0..100 { + widgets.push(AwesomeButton::new()); + } + for widget in &widgets { + flow_box.append(widget); + } + + workbench::preview(&container) +} diff --git a/src/Library/demos/Custom Widget/main.py b/src/Library/demos/Custom Widget/main.py new file mode 100644 index 000000000..fa248cc9f --- /dev/null +++ b/src/Library/demos/Custom Widget/main.py @@ -0,0 +1,28 @@ +import gi + +gi.require_version("Gtk", "4.0") + +from gi.repository import Gtk +import workbench + + +@Gtk.Template(string=workbench.template) +class AwesomeButton(Gtk.Button): + # This is normally just "AwesomeButton" as defined in the XML/Blueprint. + # In your actual code, just put that here. We need to do it like this for technical reasons. + __gtype_name__ = workbench.template_gtype_name + + @Gtk.Template.Callback() + def onclicked(self, _button): + print("Clicked") + + +container = Gtk.ScrolledWindow() +flow_box = Gtk.FlowBox(hexpand=True) +container.set_child(flow_box) + +for _ in range(100): + widget = AwesomeButton() + flow_box.append(widget) + +workbench.preview(container) diff --git a/src/Library/demos/Custom Widget/main.vala b/src/Library/demos/Custom Widget/main.vala new file mode 100644 index 000000000..1c64ec369 --- /dev/null +++ b/src/Library/demos/Custom Widget/main.vala @@ -0,0 +1,25 @@ +#!/usr/bin/env -S vala workbench.vala workbench.Resource.c --gresources=workbench_demo.xml --pkg gtk4 + +// The resource will be provided by Workbench when the demo compiles, see shebang above. +[GtkTemplate (ui = "/re/sonny/Workbench/demo/workbench_template.ui")] +public class AwesomeButton : Gtk.Button { + [GtkCallback] + private void onclicked (Gtk.Button button) { + message ("Clicked"); + } +} + +public void main () { + var container = new Gtk.ScrolledWindow(); + var flow_box = new Gtk.FlowBox() { + hexpand = true + }; + container.set_child(flow_box); + + for (var i = 0; i < 100; i++) { + var widget = new AwesomeButton(); + flow_box.append(widget); + } + + workbench.preview(container); +} diff --git a/src/Previewer/External.js b/src/Previewer/External.js index a85b9618b..f04efb8e7 100644 --- a/src/Previewer/External.js +++ b/src/Previewer/External.js @@ -1,5 +1,6 @@ import Adw from "gi://Adw"; import dbus_previewer from "./DBusPreviewer.js"; +import { decode } from "../util.js"; export default function External({ output, builder, onWindowChange }) { const stack = builder.get_object("stack_preview"); @@ -57,9 +58,21 @@ export default function External({ output, builder, onWindowChange }) { .catch(console.error); } - async function updateXML({ xml, target_id, original_id }) { + async function updateXML({ + xml, + target_id, + original_id, + template_gtype_name, + template, + }) { try { - await dbus_proxy.UpdateUiAsync(xml, target_id, original_id || ""); + await dbus_proxy.UpdateUiAsync( + xml, + target_id, + original_id || "", + template_gtype_name || "", + template ? decode(template) : "", + ); } catch (err) { console.debug(err); } diff --git a/src/Previewer/Previewer.js b/src/Previewer/Previewer.js index 732ac57a5..20e855bf3 100644 --- a/src/Previewer/Previewer.js +++ b/src/Previewer/Previewer.js @@ -168,6 +168,7 @@ export default function Previewer({ let tree; let original_id; let template; + let template_gtype_name; if (!text) { text = `";`; @@ -175,7 +176,8 @@ export default function Previewer({ try { tree = xml.parse(text); - ({ target_id, text, original_id, template } = targetBuildable(tree)); + ({ target_id, text, original_id, template, template_gtype_name } = + targetBuildable(tree)); } catch (err) { // console.error(err); console.debug(err); @@ -224,17 +226,20 @@ export default function Previewer({ } dropdown_preview_align.visible = !!template; - await current.updateXML({ + const update_xml_params = { xml: text, builder, object_preview, target_id, original_id, template, - }); + template_gtype_name, + }; + await current.updateXML(update_xml_params); code_view_css.clearDiagnostics(); await current.updateCSS(code_view_css.buffer.text); symbols = null; + return update_xml_params; } const schedule_update = unstack(update, console.error); @@ -369,14 +374,12 @@ function getTemplate(tree) { const original = tree.toString(); tree.remove(template); + // Insert a dummy target back in. const target_id = makeWorkbenchTargetId(); const el = new xml.Element("object", { - class: parent, + class: "GtkBox", id: target_id, }); - template.children.forEach((child) => { - el.cnode(child); - }); tree.cnode(el); return { @@ -384,6 +387,7 @@ function getTemplate(tree) { text: tree.toString(), original_id: undefined, template: encode(original), + template_gtype_name: template.attrs.class, }; } @@ -409,7 +413,13 @@ function targetBuildable(tree) { const target_id = makeWorkbenchTargetId(); child.attrs.id = target_id; - return { target_id, text: tree.toString(), original_id, template: null }; + return { + target_id, + text: tree.toString(), + original_id, + template: null, + template_gtype_name: null, + }; } function makeSignalHandler( diff --git a/src/Previewer/TemplateResource.js b/src/Previewer/TemplateResource.js new file mode 100644 index 000000000..437e03a8a --- /dev/null +++ b/src/Previewer/TemplateResource.js @@ -0,0 +1,57 @@ +import Gio from "gi://Gio"; +import { encode } from "../util.js"; + +function files(demoDirectory) { + return { + template_ui: demoDirectory.get_child("workbench_template.ui"), + gresource: demoDirectory.get_child("workbench_demo.xml"), + }; +} + +function generateGresource(templateName) { + return ` + + + ${templateName} + +`; +} + +/** + * Utilities for writing composite templates to the demo session directory and generating a gresource file for it. + * This can be used by external previewers that need this information at compile time and can not retrieve the + * template via D-Bus + the previewer process. + */ +const template_resource = { + async generateTemplateResourceFile(sessionDirectory) { + const { template_ui, gresource } = files(sessionDirectory); + await gresource.replace_contents_async( + encode(generateGresource(template_ui.get_basename())), + null, + false, + Gio.FileCreateFlags.NONE, + null, + ); + return gresource.get_path(); + }, + async writeTemplateUi(sessionDirectory, templateContents) { + if (templateContents === null) { + // If we don't have a template, we still generate an empty file to (try to) avoid confusing compiler errors if + // the user tries to access the file via gresource. + templateContents = encode( + '', + ); + } + + const { template_ui } = files(sessionDirectory); + await template_ui.replace_contents_async( + encode(templateContents), + null, + false, + Gio.FileCreateFlags.NONE, + null, + ); + }, +}; + +export default template_resource; diff --git a/src/Previewer/previewer.vala b/src/Previewer/previewer.vala index 21e1389eb..4bfb331ab 100644 --- a/src/Previewer/previewer.vala +++ b/src/Previewer/previewer.vala @@ -65,7 +65,10 @@ namespace Workbench { typeof (WebKit.WebView).ensure(); } - public void update_ui (string content, string target_id, string original_id = "") { + public void update_ui (string content, string target_id, string original_id = "", string template_gtype_name = "", string template = "") { + // we don't use template: This is compiled as a gresource for Vala and for Rust it's read directly + // and compiled from a path to the UI file. See Rust/Vala compiler. + this.ensure_types(); this.builder = new Gtk.Builder.from_string (content, content.length); var target = this.builder.get_object (target_id) as Gtk.Widget; @@ -188,6 +191,10 @@ namespace Workbench { Gtk.Window.set_interactive_debugging (enabled); } + public void preview() { + + } + public Adw.ColorScheme ColorScheme { get; set; default = Adw.ColorScheme.DEFAULT; } public signal void window_open (bool open); diff --git a/src/Previewer/previewer.xml b/src/Previewer/previewer.xml index cd194af4e..1dceeec1e 100644 --- a/src/Previewer/previewer.xml +++ b/src/Previewer/previewer.xml @@ -4,6 +4,8 @@ + + diff --git a/src/langs/python/python-previewer.py b/src/langs/python/python-previewer.py index 0ed4ef5a4..20182e7e4 100644 --- a/src/langs/python/python-previewer.py +++ b/src/langs/python/python-previewer.py @@ -51,6 +51,8 @@ class Previewer: builder: Gtk.Builder | None target: Gtk.Widget | None css: Gtk.CssProvider | None + template: str | None + template_gtype_name: str | None uri = str | None style_manager: Adw.StyleManager @@ -60,11 +62,22 @@ def __init__(self): self.window = None self.builder = None self.target = None + self.template = None + self.template_gtype_name = None self.uri = None @DBusTemplate.Method() - def update_ui(self, content: str, target_id: str, original_id: str = ""): + def update_ui( + self, + content: str, + target_id: str, + original_id: str = "", + template_gtype_name: str = "", + template: str = "", + ): self.builder = Gtk.Builder.new_from_string(content, len(content)) + self.template = template + self.template_gtype_name = template_gtype_name target = self.builder.get_object(target_id) if target is None: print( @@ -233,6 +246,11 @@ def on_css_parsing_error(self, _css, section: Gtk.CssSection, error: GLib.Error) def resolve(self, path: str): return Gio.File.new_for_uri(self.uri).resolve_relative_path(path).get_uri() + def preview(self, widget: Gtk.Widget): + self.target = widget + self.ensure_window() + self.window.set_child(widget) + # 3. API for demos # ---------------- @@ -247,7 +265,7 @@ def __init__(self, previewer: Previewer): self._previewer = previewer def __getattr__(self, name): - # Getting `window` or `builder` will transparently forward to calls + # Getting `window` `builder`, etc. will transparently forward to calls # `window`/`builder` attributes of the previewer. # We do this to make the API in the demos a bit simpler. Just using a normal module's dict and @@ -261,6 +279,12 @@ def __getattr__(self, name): return self._previewer.builder if name == "resolve": return self._previewer.resolve + if name == "template": + return self._previewer.template + if name == "template_gtype_name": + return self._previewer.template_gtype_name + if name == "preview": + return self._previewer.preview raise KeyError diff --git a/src/langs/rust/Compiler.js b/src/langs/rust/Compiler.js index 4f479b414..a22c83985 100644 --- a/src/langs/rust/Compiler.js +++ b/src/langs/rust/Compiler.js @@ -2,6 +2,7 @@ import Gio from "gi://Gio"; import GLib from "gi://GLib"; import dbus_previewer from "../../Previewer/DBusPreviewer.js"; import { decode, encode } from "../../util.js"; +import template_resource from "../../Previewer/TemplateResource.js"; export default function Compiler({ session }) { const { file } = session; @@ -14,7 +15,7 @@ export default function Compiler({ session }) { let rustcVersion; let savedRustcVersion; - async function compile() { + async function compile(template) { rustcVersion ||= await getRustcVersion(); savedRustcVersion ||= await getSavedRustcVersion({ rustcVersionFile }); @@ -23,6 +24,8 @@ export default function Compiler({ session }) { await saveRustcVersion({ targetPath, rustcVersion, rustcVersionFile }); } + await template_resource.writeTemplateUi(file, template); + const cargo_launcher = new Gio.SubprocessLauncher(); cargo_launcher.set_cwd(file.get_path()); diff --git a/src/langs/rust/template/workbench.rs b/src/langs/rust/template/workbench.rs index 585e74b4a..44e3f9f08 100644 --- a/src/langs/rust/template/workbench.rs +++ b/src/langs/rust/template/workbench.rs @@ -33,3 +33,12 @@ pub(crate) fn resolve(path: impl AsRef) -> String { .to_string() } } + +#[allow(dead_code)] +pub(crate) fn preview(widget: &impl IsA) { + // TODO: We would now need to actually somehow communicate back to the + // previewer.vala itself to set it's target and ensure_window... + // this.target = widget; + // this.ensure_window(); + // this.window.set_child(widget); +} diff --git a/src/langs/vala/Compiler.js b/src/langs/vala/Compiler.js index 6590493f8..66b3baa93 100644 --- a/src/langs/vala/Compiler.js +++ b/src/langs/vala/Compiler.js @@ -1,6 +1,7 @@ import Gio from "gi://Gio"; import dbus_previewer from "../../Previewer/DBusPreviewer.js"; import { decode } from "../../util.js"; +import template_resource from "../../Previewer/TemplateResource.js"; export default function ValaCompiler({ session }) { const { file } = session; @@ -8,7 +9,7 @@ export default function ValaCompiler({ session }) { const module_file = file.get_child("libworkbenchcode.so"); const file_vala = file.get_child("main.vala"); - async function compile() { + async function compile(template) { let args; try { @@ -20,6 +21,12 @@ export default function ValaCompiler({ session }) { return; } + // We now also need to write and compile the resource file first. + const gresource_path = + await template_resource.generateTemplateResourceFile(file); + await template_resource.writeTemplateUi(file, template); + await compileGresource(gresource_path); + const valac_launcher = new Gio.SubprocessLauncher(); valac_launcher.set_cwd(file.get_path()); const valac = valac_launcher.spawnv([ @@ -46,6 +53,23 @@ export default function ValaCompiler({ session }) { return result; } + async function compileGresource(gresource_path) { + const glib_compile_launcher = new Gio.SubprocessLauncher(); + glib_compile_launcher.set_cwd(file.get_path()); + const glib_compile = glib_compile_launcher.spawnv([ + "glib-compile-resources", + gresource_path, + "--generate-source", + "--target=workbench.Resource.c", + ]); + + await glib_compile.wait_async(null); + + const result = glib_compile.get_successful(); + glib_compile_launcher.close(); + return result; + } + async function run() { try { const proxy = await dbus_previewer.getProxy("vala"); diff --git a/src/langs/vala/workbench.vala b/src/langs/vala/workbench.vala index dfb3a301b..54d7ab3d7 100644 --- a/src/langs/vala/workbench.vala +++ b/src/langs/vala/workbench.vala @@ -2,6 +2,13 @@ namespace workbench { public static Gtk.Builder builder; public static Gtk.Window window; public static string uri; + public void preview (Gtk.Widget widget) { + // TODO: We would now need to actually somehow communicate back to the + // previewer.vala itself to set it's target and ensure_window... + // this.target = widget; + // this.ensure_window(); + // this.window.set_child(widget); + } public string resolve (string path) { return File.new_for_uri(workbench.uri).resolve_relative_path(path).get_uri(); } diff --git a/src/window.js b/src/window.js index 0ba7cefb6..5af3c63d6 100644 --- a/src/window.js +++ b/src/window.js @@ -378,10 +378,12 @@ export default function Window({ application, session }) { return; } + await previewer.useExternal("vala"); + const { template } = await previewer.update(true); + compiler_vala = compiler_vala || ValaCompiler({ session }); - const success = await compiler_vala.compile(); + const success = await compiler_vala.compile(template); if (success) { - await previewer.useExternal("vala"); if (await compiler_vala.run()) { await previewer.open(); } else { @@ -394,10 +396,12 @@ export default function Window({ application, session }) { return; } + await previewer.useExternal("rust"); + const { template } = await previewer.update(true); + compiler_rust = compiler_rust || RustCompiler({ session }); - const success = await compiler_rust.compile(); + const success = await compiler_rust.compile(template); if (success) { - await previewer.useExternal("rust"); if (await compiler_rust.run()) { await previewer.open(); } else {