diff --git a/examples/widget-template-manual/box.blp b/examples/widget-template-manual/box.blp new file mode 100644 index 0000000..25a6e87 --- /dev/null +++ b/examples/widget-template-manual/box.blp @@ -0,0 +1,11 @@ +using Gtk 4.0; + +template $My_Box: Gtk.Box { + halign: center; + valign: center; + + Button my-button { + label: _("Hi! Click me and look at the terminal!"); + clicked => $my_button_clicked(); + } +} diff --git a/examples/widget-template-manual/box.gresource b/examples/widget-template-manual/box.gresource new file mode 100644 index 0000000..42fb63b Binary files /dev/null and b/examples/widget-template-manual/box.gresource differ diff --git a/examples/widget-template-manual/box.gresource.xml b/examples/widget-template-manual/box.gresource.xml new file mode 100644 index 0000000..8f92139 --- /dev/null +++ b/examples/widget-template-manual/box.gresource.xml @@ -0,0 +1,5 @@ + + + box.ui + + diff --git a/examples/widget-template-manual/box.ui b/examples/widget-template-manual/box.ui new file mode 100644 index 0000000..df7158a --- /dev/null +++ b/examples/widget-template-manual/box.ui @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/examples/widget-template-manual/window.odin b/examples/widget-template-manual/window.odin new file mode 100644 index 0000000..3c3f891 --- /dev/null +++ b/examples/widget-template-manual/window.odin @@ -0,0 +1,147 @@ +package template + +@require import "base:runtime" +@require import "core:fmt" +@require import adw "../../adwaita" +@require import "../../glib" +@require import gobj "../../glib/gobject" +@require import "../../glib/gio" +@require import "../../gtk" + +// Return the GType of our widget, and register it if it isn't already. +my_box_get_type :: proc "c" () -> (g_type: gobj.Type) { + @static static_g_type: gobj.Type + + // If the type is already registered, we just return the type id. + if static_g_type != 0 { return static_g_type } + + info := gobj.TypeInfo{ + class_size = size_of(My_Box_Class), + class_init = class_init, + + instance_size = size_of(My_Box), + instance_init = instance_init, + } + // We the type id so it sticks. + static_g_type = gobj.type_register_static(gtk.box_get_type(), "My_Box", &info, .NONE) + g_type = static_g_type + return + + // Constructors + // These could be defined outside of this proc too, but it's nicer to have them contained here. + + // This handles everything that's related to the overall widget class. + class_init :: proc "c" (class: glib.pointer, data: glib.pointer) { + gtk.widget_class_set_template_from_resource(cast(^gtk.WidgetClass)class, "/example/box.ui") + + // We can set the click action here. See the blueprint `box.blp` for more information. + // Alternatively, we could set it in the `instance_init` proc as well. + widget_class := cast(^gtk.WidgetClass)class + offset := cast(glib.ssize)offset_of(My_Box, button) + + gtk.widget_class_bind_template_child_full( + widget_class, + name = "my-button", + internal_child = false, // I have no idea what this does. Both work (shrug). + struct_offset = offset, // This will automatically bind our button into our My_Box struct. How cool! + ) + + // We set up our button's clicked action defined in the blueprint. + // Refer to the `instance_init` proc if you want to set it without blueprint. + clicked_callback := cast(gobj.Callback)my_button_clicked + gtk.widget_class_bind_template_callback_full(widget_class, "my_button_clicked", clicked_callback) + + // Rest of your class setup and virtual methods etc. go here as well. + } + + // This handles the individual instances of the widget. + instance_init :: proc "c" (instance: ^gobj.TypeInstance, class_data: glib.pointer) { + gtk.widget_init_template(cast(^gtk.Widget)instance) + + /* + // We could also set up our child button here instead, if we wanted to. + + button := cast(^gtk.Button)( + gtk.widget_get_template_child(cast(^gtk.Widget)instance, my_button_get_type(), "my-button") + ) + my_box := cast(^My_Box)instance + my_box.button = button + + // It is, however, important to note that we would need to unset the `my_button_clicked` signal + // in the blueprint. It's either-or. Both cannot be true at once, GTK will yell at us otherwise. + + clicked_callback := cast(gobj.Callback)my_button_clicked + gobj.signal_connect(button, "clicked", clicked_callback, nil) + */ + + // Rest of your instance overrides and setup go here as well. + } + + // This will get called when we click the button. + my_button_clicked :: proc "c"(button: ^gtk.Button, data: glib.pointer) { + parent := gtk.widget_get_parent(cast(^gtk.Widget)button) + my_box := cast(^My_Box)parent + + my_box.button_clicked += 1 + + context = runtime.default_context() + fmt.printfln("Button clicked %d times!", my_box.button_clicked) + + // GTK copies our string, so we are responsible for freeing the memory. + button_clicked_cstring := fmt.caprintf("Clicked %d times!", my_box.button_clicked) + defer delete(button_clicked_cstring) + + gtk.button_set_label(button, button_clicked_cstring) + } +} + +// Needed by GTK internally. +My_Box_Class :: struct { + parent_class: gtk.BoxClass, +} + +// The box instance. +// Note: `parent` field has to be the first. +My_Box :: struct { + parent_instance: gtk.Box, + button_clicked: int, + button: ^gtk.Button, // this field is set by us in the `class_init` proc. +} + +main :: proc() { + context = glib.create_context() + + // We must initialise GTK, otherwise our parent class, `gtk.Box` is not valid. + gtk.init() + + // We load the resource file that was compiled by `glib-compile-resources` + // The `.ui` file was created by blueprint-compiler. + // use the `#load` directive for release builds, to bundle it into the app. + resource := gio.resource_load("box.gresource", nil) + gio.resources_register(resource) + + app := adw.application_new("some.example", .NONE) + defer gobj.object_unref(app) + + gobj.signal_connect(app, "activate", cast(gobj.Callback)show_window, nil) + status := gio.application_run(cast(^gio.Application)app, 0, nil) + + if status != 0 { + fmt.eprintfln("%#v", "oh no!") + } +} + +// We set up and show the main window here. This could be split up to multiple procs, obviously. +show_window :: proc "c" (app: ^adw.Application) { + // GTK is very cast heavy, unfortunately. + // I like to prefix generic variables with `_` as a note to myself. + _window := adw.application_window_new(cast(^gtk.Application)(app)) + window := cast(^adw.ApplicationWindow)_window + + // We create our custom box here, initialised as per its init function. + my_box_g_type := my_box_get_type() + _my_box := gobj.object_new(my_box_g_type, nil) + + adw.application_window_set_content(window, cast(^gtk.Widget)_my_box) + gtk.window_present(cast(^gtk.Window)_window) +} diff --git a/examples/widget-template-with-helper/box.blp b/examples/widget-template-with-helper/box.blp new file mode 100644 index 0000000..12087dc --- /dev/null +++ b/examples/widget-template-with-helper/box.blp @@ -0,0 +1,11 @@ +using Gtk 4.0; + +template $My_Box: Gtk.Box { + halign: center; + valign: center; + + Button my-button { + label: bind template.button-label; + clicked => $my_button_clicked(); + } +} diff --git a/examples/widget-template-with-helper/box.gresource b/examples/widget-template-with-helper/box.gresource new file mode 100644 index 0000000..0f9ec1a Binary files /dev/null and b/examples/widget-template-with-helper/box.gresource differ diff --git a/examples/widget-template-with-helper/box.gresource.xml b/examples/widget-template-with-helper/box.gresource.xml new file mode 100644 index 0000000..8f92139 --- /dev/null +++ b/examples/widget-template-with-helper/box.gresource.xml @@ -0,0 +1,5 @@ + + + box.ui + + diff --git a/examples/widget-template-with-helper/box.ui b/examples/widget-template-with-helper/box.ui new file mode 100644 index 0000000..a2ae0c5 --- /dev/null +++ b/examples/widget-template-with-helper/box.ui @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/examples/widget-template-with-helper/window.odin b/examples/widget-template-with-helper/window.odin new file mode 100644 index 0000000..dc95644 --- /dev/null +++ b/examples/widget-template-with-helper/window.odin @@ -0,0 +1,144 @@ +package template + +@require import "base:runtime" +@require import "core:fmt" +@require import adw "../../adwaita" +@require import "../../glib" +@require import gobj "../../glib/gobject" +@require import "../../glib/gio" +@require import "../../gtk" + +// The box instance. +// Note: `parent` field has to be the first. +My_Box :: struct { + parent_instance: gtk.Box, + button_clicked: int, + button: ^gtk.Button, // This field is populated automatically. + button_label: cstring `gproperty:"1,button-label"`, +} + +main :: proc() { + context = glib.create_context() + + // We must initialise GTK, otherwise our parent class, `gtk.Box` is not valid. + gtk.init() + + // We load the resource file that was compiled by `glib-compile-resources` + // The `.ui` file was created by blueprint-compiler. + // use the `#load` directive for release builds, to bundle it into the app. + resource := gio.resource_load("box.gresource", nil) + gio.resources_register(resource) + + app := adw.application_new("some.example", .NONE) + defer gobj.object_unref(app) + + gobj.signal_connect(app, "activate", cast(gobj.Callback)show_window, nil) + status := gio.application_run(cast(^gio.Application)app, 0, nil) + + if status != 0 { + fmt.eprintfln("%#v", "oh no!") + } + } + +// We set up and show the main window here. This could be split up to multiple procs, obviously. +show_window :: proc "c" (app: ^adw.Application) { + // GTK is very cast heavy, unfortunately. + // I like to prefix generic variables with `_` as a note to myself. + _window := adw.application_window_new(cast(^gtk.Application)(app)) + window := cast(^adw.ApplicationWindow)_window + + // Context is needed for reflection used by the setup. + context = runtime.default_context() + + // If we only care about registering the child, and don't want to bind it to our struct, + // we can leave `field_name` empty. + template_children := []gtk.Template_Child{ + { + id = "my-button", + field_name = "button", + }, + } + // See gtk.Template_Data for more information about what this is. + template_data := gtk.Template_Data{ + resource_path = "/example/box.ui", + children = template_children, + type = My_Box, + } + gtk.register_type_with_template( + My_Box, + gtk.BoxClass, + gtk.box_get_type(), + &template_data, + + // We want to set a `clicked` signal to our button, so we must supply our own init proc. + class_init_proc = my_box_class_init, + instance_init_proc = my_box_instance_init, + ) + + // We create our custom box here, initialised as per its init function. + my_box_g_type := gtk.custom_type_get_type(My_Box) + _my_box := gobj.object_new(my_box_g_type, nil) + + adw.application_window_set_content(window, cast(^gtk.Widget)_my_box) + gtk.window_present(cast(^gtk.Window)_window) +} + +// Our custom class init proc, since we want to set the `clicked` action on our button. +my_box_class_init :: proc "c" (class: glib.pointer, data: glib.pointer) { + // We register the getter and setter procs for our box's custom gproperties. + object_class := cast(^gobj.ObjectClass)class + object_class.get_property = my_box_property_get + object_class.set_property = my_box_property_set + + // We call the default init proc, so that we don't have to do the child binding and template setup ourselves. + // This takes care of common use cases like registering children and annotated gproperties. + gtk.class_init_default_template(class, data) + + // We register our proc as a callback to the signal defined in the blueprint. + // Alternatively, we could register the signal in the `instance_init` proc too. + widget_class := cast(^gtk.WidgetClass)class + clicked_callback := cast(gobj.Callback)my_button_clicked + gtk.widget_class_bind_template_callback_full(widget_class, "my_button_clicked", clicked_callback) +} + +// This is called by GTK whenever it wants to know the value of any of the registered properties. +my_box_property_get :: proc "c"(object: ^gobj.Object, property_id: glib.uint_, value: ^gobj.Value, pspec: ^gobj.ParamSpec) { + my_box := cast(^My_Box)object + switch property_id { + case 1: // We set the id of our property to 1. + gobj.value_set_string(value, my_box.button_label) + } +} + +my_box_instance_init :: proc "c" (instance: ^gobj.TypeInstance, class_data: glib.pointer) { + // We call the default template to do the setup. + gtk.instance_init_default_template(instance, class_data) + + // We set a default value for our label, since the binding sets none by itself. + my_box := cast(^My_Box)instance + gtk.button_set_label(my_box.button, "Click me!") +} + +// We can leave this empty for now because we don't plan on setting it through GTK. +my_box_property_set :: proc "c"(object: ^gobj.Object, property_id: glib.uint_, value: ^gobj.Value, pspec: ^gobj.ParamSpec) {} + +// This will get called when we click the button. +my_button_clicked :: proc "c"(button: ^gtk.Button, data: glib.pointer) { + parent := gtk.widget_get_parent(cast(^gtk.Widget)button) + my_box := cast(^My_Box)parent + + my_box.button_clicked += 1 + + context = runtime.default_context() + fmt.printfln("Button clicked %d times!", my_box.button_clicked) + + button_clicked_cstring := fmt.caprintf("Clicked %d times!", my_box.button_clicked) + + // We set the property of our box, and tell GTK that it changed, which will automatically update everything that depends on it. + delete(my_box.button_label) + my_box.button_label = button_clicked_cstring + gobj.object_notify(cast(^gobj.Object)my_box, "button-label") + + // In this case, we could have set the button label directly too, via: + // gtk.button_set_label(button, button_clicked_cstring) +} diff --git a/glib/glib.odin b/glib/glib.odin index 9b034e0..97174dc 100644 --- a/glib/glib.odin +++ b/glib/glib.odin @@ -365,8 +365,8 @@ _GHook :: struct { } PollFD :: _GPollFD PollFunc :: #type proc "c" (ufds: [^]PollFD, nfsd: uint_, timeout_: int_) -> int_ -SList :: _GSList -_GSList :: struct { +// SList :: _GSList +SList :: struct { data: pointer, next: ^SList, } @@ -7650,11 +7650,11 @@ when (ODIN_OS == .Linux) && (ODIN_ARCH == .amd64) { when #config(GLIB_STATIC, false) { when (ODIN_OS == .Linux) && (ODIN_ARCH == .amd64) { foreign import glib_runic { "../lib/linux/x86_64/libglib-2.0.a", "../lib/linux/x86_64/libglib-wrapper.a", "system:ffi", "system:pcre2-8" } -} +} } else { when (ODIN_OS == .Linux) && (ODIN_ARCH == .amd64) { foreign import glib_runic { "system:glib-2.0", "../lib/linux/x86_64/libglib-wrapper.a" } -} +} } } @@ -7664,11 +7664,11 @@ when (ODIN_OS == .Linux) && (ODIN_ARCH == .arm64) { when #config(GLIB_STATIC, false) { when (ODIN_OS == .Linux) && (ODIN_ARCH == .arm64) { foreign import glib_runic { "../lib/linux/aarch64/libglib-2.0.a", "../lib/linux/aarch64/libglib-wrapper.a", "system:ffi", "system:pcre2-8" } -} +} } else { when (ODIN_OS == .Linux) && (ODIN_ARCH == .arm64) { foreign import glib_runic { "system:glib-2.0", "../lib/linux/aarch64/libglib-wrapper.a" } -} +} } } @@ -7677,7 +7677,6 @@ when (ODIN_OS == .Windows) && (ODIN_ARCH == .amd64) { when (ODIN_OS == .Windows) && (ODIN_ARCH == .amd64) { foreign import glib_runic { "../lib/windows/x86_64/glib-2.0.lib", "../lib/windows/x86_64/glib-wrapper.lib" } -} - } +} diff --git a/gtk/template_helper.odin b/gtk/template_helper.odin new file mode 100644 index 0000000..88a4405 --- /dev/null +++ b/gtk/template_helper.odin @@ -0,0 +1,339 @@ +package gtk + +import "core:strconv" +import "core:strings" +import "base:runtime" +import "core:reflect" +@require import "core:fmt" +import gobj "../glib/gobject" +import "../glib/" + +Template_Data :: struct { + // URI of the `.ui` file in the gresource. + // Must be registered in advance with `gio.resources_register` + resource_path: cstring, + + // A slice of template children you'd like registered. + children: []Template_Child, + + // The owning type. + type: typeid, +} + +Template_Child :: struct { + // The id in the `.ui` file. + id: cstring, + + // The field name in our strict we want to bind it to. + // If empty, it won't be bound. + field_name: string, +} + +/* +This must be called once before you do anything with your custom widget, +otherwise it is not defined in the gobject typesystem! + +Default class and instance init procs don't do anything, so if you need subtype binding or some other setup, +you need to supply them yourself. +*/ +register_type :: proc( + $instance_type: typeid, + $parent_class_type: typeid, + parent_g_type: gobj.Type, + instance_init_proc: gobj.InstanceInitFunc = instance_init_default, + class_init_proc: gobj.ClassInitFunc = class_init_default, +) -> ( + g_type: gobj.Type, +) { + + static_g_type_ptr := custom_type_get_type_ptr(instance_type) + static_g_type := static_g_type_ptr^ + if static_g_type != 0 { return static_g_type } + + info := gobj.TypeInfo{ + class_size = size_of(Custom_Type_Class), + class_init = class_init_proc, + + instance_size = size_of(instance_type), + instance_init = instance_init_proc, + } + + my_type_name := fmt.caprint(typeid_of(instance_type)) + defer delete(my_type_name) + registered_g_type := gobj.type_register_static(parent_g_type, my_type_name, &info, .NONE) + + static_g_type_ptr^ = registered_g_type + g_type = registered_g_type + return + + Custom_Type_Class :: struct { + parent_class: parent_class_type, + } +} + +/* +This must be called once before you do anything with your custom widget, +otherwise it is not defined in the gobject typesystem! + +Default class and instance init procs don't do anything, so if you need subtype binding or some other setup, +you need to supply them yourself. + +The default instance init proc does not depend on the template data, so it can safely be on the stack. + +**Warning**: If your instance init proc depends on the template data, you must not free it while you +keep making widgets of this type! +*/ +register_type_with_template :: proc( + $instance_type: typeid, + $parent_class_type: typeid, + parent_g_type: gobj.Type, + template_data: ^Template_Data, + instance_init_proc: gobj.InstanceInitFunc = instance_init_default_template, + class_init_proc: gobj.ClassInitFunc = class_init_default_template, +) -> ( + g_type: gobj.Type, +) { + static_g_type_ptr := custom_type_get_type_ptr(instance_type) + static_g_type := static_g_type_ptr^ + if static_g_type != 0 { return static_g_type } + + info := gobj.TypeInfo{ + class_size = size_of(Custom_Type_Class), + class_init = class_init_proc, + class_data = template_data, + + instance_size = size_of(instance_type), + instance_init = instance_init_proc, + } + + my_type_name := fmt.caprint(typeid_of(instance_type)) + defer delete(my_type_name) + + registered_g_type := gobj.type_register_static(parent_g_type, my_type_name, &info, .NONE) + + static_g_type_ptr^ = registered_g_type + g_type = registered_g_type + return + + Custom_Type_Class :: struct { + parent_class: parent_class_type, + } +} + +// Returns the `gobject.Type` of the custom widget. +// +// Note: Widget must already be registered in the type system via `register_type` or +// `register_type_from_template`. +@(require_results) +custom_type_get_type :: proc($instance_type: typeid) -> (g_type: gobj.Type) { + g_type_ptr := custom_type_get_type_ptr(instance_type) + g_type = g_type_ptr^ + return +} + +// Helper proc to store the internal `gobject.Type` for each custom type. Needs to be set from +// one of the register procs. +@(private, require_results) +custom_type_get_type_ptr :: proc($instance_type: typeid) -> (g_type: ^gobj.Type) { + @static static_g_type: gobj.Type + g_type = &static_g_type + return +} + +@private +instance_init_default :: proc "c" (instance: ^gobj.TypeInstance, class_data: glib.pointer) {} + +@private +class_init_default :: proc "c" (class: glib.pointer, data: glib.pointer) {} + +// Takes care of necessary initialisation, like binding children and setting up the template. +// You can safely call this at the beginning of your own implementation too. +instance_init_default_template :: proc "c" (instance: ^gobj.TypeInstance, class_data: glib.pointer) { + widget_init_template(cast(^Widget)instance) +} + +// Takes care of necessary initialisation, like binding children and setting up the template. +// You can safely call this at the beginning of your own implementation too. +class_init_default_template :: proc "c" (class: glib.pointer, data: glib.pointer) { + widget_class := cast(^WidgetClass)class + template_data := cast(^Template_Data)data + + widget_class_set_template_from_resource(widget_class, template_data.resource_path) + + // Required by `reflect`. + context = runtime.default_context() + + for child in template_data.children { + field := reflect.struct_field_by_name(template_data.type, child.field_name) + + widget_class_bind_template_child_full( + widget_class, + name = child.id, + internal_child = false, + struct_offset = cast(glib.ssize)field.offset, + ) + } + + object_class := cast(^gobj.ObjectClass)class + fields := reflect.struct_fields_zipped(template_data.type) + for field in fields { + // We need to get the id and the property name, in the format "3,my-property-name" + tag, ok := reflect.struct_tag_lookup(field.tag, "gproperty") + if ok { + sep_index := strings.index_rune(tag, ',') + if sep_index == -1 { continue } + + param_spec, spec_ok := create_param_spec(field.type.id, tag[sep_index + 1:]) + if !spec_ok { continue } + + id, id_ok := strconv.parse_uint(tag[:sep_index]) + if !id_ok { continue } + + gobj.object_class_install_property(object_class, u32(id), param_spec) + } + } + + create_param_spec :: proc(type: typeid, tag: string) -> (param_spec: ^gobj.ParamSpec, ok: bool) { + ok = true + name := fmt.caprint(tag) + defer delete(name) + + switch type { + case string: + param_spec = gobj.param_spec_string( + name = name, + nick = nil, + blurb = nil, + default_value = nil, + flags = gobj.ParamFlags.READWRITE, + ) + case cstring: + param_spec = gobj.param_spec_string( + name = name, + nick = nil, + blurb = nil, + default_value = nil, + flags = gobj.ParamFlags.READWRITE, + ) + case bool: + param_spec = gobj.param_spec_boolean( + name = name, + nick = nil, + blurb = nil, + default_value = false, + flags = gobj.ParamFlags.READWRITE, + ) + case i8: + param_spec = gobj.param_spec_char( + name = name, + nick = nil, + blurb = nil, + minimum = min(i8), + maximum = max(i8), + default_value = 0, + flags = gobj.ParamFlags.READWRITE, + ) + case u8: + param_spec = gobj.param_spec_uchar( + name = name, + nick = nil, + blurb = nil, + minimum = min(u8), + maximum = max(u8), + default_value = 0, + flags = gobj.ParamFlags.READWRITE, + ) + case i32: + param_spec = gobj.param_spec_int( + name = name, + nick = nil, + blurb = nil, + minimum = min(i32), + maximum = max(i32), + default_value = 0, + flags = gobj.ParamFlags.READWRITE, + ) + case u32: + param_spec = gobj.param_spec_uint( + name = name, + nick = nil, + blurb = nil, + minimum = min(u32), + maximum = max(u32), + default_value = 0, + flags = gobj.ParamFlags.READWRITE, + ) + case i64: + param_spec = gobj.param_spec_int64( + name = name, + nick = nil, + blurb = nil, + minimum = min(i64), + maximum = max(i64), + default_value = 0, + flags = gobj.ParamFlags.READWRITE, + ) + case int: + param_spec = gobj.param_spec_int64( + name = name, + nick = nil, + blurb = nil, + minimum = min(i64), + maximum = max(i64), + default_value = 0, + flags = gobj.ParamFlags.READWRITE, + ) + case u64: + param_spec = gobj.param_spec_uint64( + name = name, + nick = nil, + blurb = nil, + minimum = min(u64), + maximum = max(u64), + default_value = 0, + flags = gobj.ParamFlags.READWRITE, + ) + case uint: + param_spec = gobj.param_spec_uint64( + name = name, + nick = nil, + blurb = nil, + minimum = min(u64), + maximum = max(u64), + default_value = 0, + flags = gobj.ParamFlags.READWRITE, + ) + case f32: + param_spec = gobj.param_spec_float( + name = name, + nick = nil, + blurb = nil, + minimum = min(f32), + maximum = max(f32), + default_value = 0, + flags = gobj.ParamFlags.READWRITE, + ) + case f64: + param_spec = gobj.param_spec_double( + name = name, + nick = nil, + blurb = nil, + minimum = min(f64), + maximum = max(f64), + default_value = 0, + flags = gobj.ParamFlags.READWRITE, + ) + case gobj.Object: + param_spec = gobj.param_spec_object( + name = name, + nick = nil, + blurb = nil, + object_type = gobj.object_get_type(), + flags = gobj.ParamFlags.READWRITE, + ) + case: ok = false + } + + return + } +}