diff options
-rw-r--r-- | gjs/src/astalify.ts | 100 | ||||
-rw-r--r-- | gjs/src/jsx/jsx-runtime.ts | 51 | ||||
-rw-r--r-- | lua/astal/binding.lua | 4 | ||||
-rw-r--r-- | lua/astal/variable.lua | 8 | ||||
-rw-r--r-- | lua/astal/widget.lua | 211 |
5 files changed, 232 insertions, 142 deletions
diff --git a/gjs/src/astalify.ts b/gjs/src/astalify.ts index f986716..ecd52d4 100644 --- a/gjs/src/astalify.ts +++ b/gjs/src/astalify.ts @@ -1,12 +1,61 @@ import Binding, { kebabify, snakeify, type Connectable, type Subscribable } from "./binding.js" import { Astal, Gtk } from "./imports.js" import { execAsync } from "./process.js" +import Variable from "./variable.js" Object.defineProperty(Astal.Box.prototype, "children", { get() { return this.get_children() }, set(v) { this.set_children(v) }, }) +function setChildren(parent: Gtk.Widget, children: Gtk.Widget[]) { + children = children.flat(Infinity).map(ch => ch instanceof Gtk.Widget + ? ch + : new Gtk.Label({ visible: true, label: String(ch) })) + + // remove + if (parent instanceof Gtk.Bin) { + const ch = parent.get_child() + if (ch) + parent.remove(ch) + } + + // FIXME: add rest of the edge cases like Stack + if (parent instanceof Astal.Box) { + parent.set_children(children) + } + + else if (parent instanceof Astal.CenterBox) { + parent.startWidget = children[0] + parent.centerWidget = children[1] + parent.endWidget = children[2] + } + + else if (parent instanceof Astal.Overlay) { + const [child, ...overlays] = children + parent.set_child(child) + parent.set_overlays(overlays) + } + + else if (parent instanceof Gtk.Container) { + for (const ch of children) + parent.add(ch) + } +} + +function mergeBindings(array: any[]) { + const getValues = () => array.map(i => i instanceof Binding ? i.get() : i) + const bindings = array.filter(i => i instanceof Binding) + + if (bindings.length === 0) + return array + + if (bindings.length === 1) + return bindings[0].as(getValues) + + return Variable.derive(bindings, getValues)() +} + export type Widget<C extends { new(...args: any): Gtk.Widget }> = InstanceType<C> & { className: string css: string @@ -47,7 +96,7 @@ function hook( return self } -function ctor(self: any, config: any = {}, ...children: Gtk.Widget[]) { +function ctor(self: any, config: any = {}, children: any[] = []) { const { setup, ...props } = config props.visible ??= true @@ -71,9 +120,6 @@ function ctor(self: any, config: any = {}, ...children: Gtk.Widget[]) { return acc }, []) - const pchildren = props.children - delete props.children - Object.assign(self, props) Object.assign(self, { hook(obj: any, sig: any, callback: any) { @@ -91,23 +137,28 @@ function ctor(self: any, config: any = {}, ...children: Gtk.Widget[]) { } } - if (self instanceof Gtk.Container) { - if (children) { - for (const child of children) - self.add(child) - } - if (pchildren && Array.isArray(pchildren)) { - for (const child of pchildren) - self.add(child) - } - } - for (const [prop, bind] of bindings) { + if (prop === "child" || prop === "children") { + self.connect("destroy", bind.subscribe((v: any) => { + setChildren(self, v) + })) + } self.connect("destroy", bind.subscribe((v: any) => { self[`set_${snakeify(prop)}`](v) })) } + children = mergeBindings(children.flat(Infinity)) + if (children instanceof Binding) { + setChildren(self, children.get()) + self.connect("destroy", children.subscribe(v => { + setChildren(self, v) + })) + } + else { + setChildren(self, children) + } + setup?.(self) return self } @@ -130,29 +181,14 @@ function proxify< set(v) { Astal.widget_set_cursor(this, v) }, }) - klass.prototype.set_child = function(widget: Gtk.Widget) { - if (this instanceof Gtk.Bin) { - const rm = this.get_child() - if (rm) - this.remove(rm) - } - if (this instanceof Gtk.Container) - this.add(widget) - } - - Object.defineProperty(klass.prototype, "child", { - get() { return this.get_child?.() }, - set(v) { this.set_child(v) }, - }) - const proxy = new Proxy(klass, { construct(_, [conf, ...children]) { const self = new klass - return ctor(self, conf, ...children) + return ctor(self, conf, children) }, apply(_t, _a, [conf, ...children]) { const self = new klass - return ctor(self, conf, ...children) + return ctor(self, conf, children) }, }) diff --git a/gjs/src/jsx/jsx-runtime.ts b/gjs/src/jsx/jsx-runtime.ts index 5e7f23b..e96f7c2 100644 --- a/gjs/src/jsx/jsx-runtime.ts +++ b/gjs/src/jsx/jsx-runtime.ts @@ -1,12 +1,5 @@ import { Gtk } from "../imports.js" import * as Widget from "../widgets.js" -import Binding from "../binding.js" - -function w(e: any) { - return e instanceof Gtk.Widget || e instanceof Binding - ? e - : Widget.Label({ label: String(e) }) -} export function jsx( ctor: keyof typeof ctors | typeof Gtk.Widget, @@ -16,46 +9,16 @@ export function jsx( if (!Array.isArray(children)) children = [children] - else - children = children.flat() - - // <box children={Binding} /> and <box>{Binding}</box> - if (ctor === "box" && children.length === 1 && children[0] instanceof Binding) { - props.children = children[0] - } - - // TODO: handle array of Binding - // is there a usecase? - - else if (ctor === "centerbox") { - if (children[0]) - props.startWidget = w(children[0]) - if (children[1]) - props.centerWidget = w(children[1]) - if (children[2]) - props.endWidget = w(children[2]) - } - - else if (ctor === "overlay") { - const [child, ...overlays] = children - if (child) - props.child = child - - props.overlays = overlays - } - else if (children.length === 1) { - props.child = w(children[0]) - delete props.children - } + if (typeof ctor === "string") + return (ctors as any)[ctor](props, children) - else { - props.children = children.map(w) - } + if (children.length === 1) + props.child = children[0] + else if (children.length > 1) + props.children = children - return typeof ctor === "string" - ? (ctors as any)[ctor](props) - : new ctor(props) + return new ctor(props) } const ctors = { diff --git a/lua/astal/binding.lua b/lua/astal/binding.lua index 264986b..c9929ea 100644 --- a/lua/astal/binding.lua +++ b/lua/astal/binding.lua @@ -2,12 +2,12 @@ local lgi = require("lgi") local GObject = lgi.require("GObject", "2.0") ---@class Binding ----@field emitter object +---@field emitter table|Variable ---@field property? string ---@field transformFn function local Binding = {} ----@param emitter object +---@param emitter table ---@param property? string ---@return Binding function Binding.new(emitter, property) diff --git a/lua/astal/variable.lua b/lua/astal/variable.lua index baa2d69..75f7d1e 100644 --- a/lua/astal/variable.lua +++ b/lua/astal/variable.lua @@ -6,11 +6,11 @@ local Time = require("astal.time") local Process = require("astal.process") ---@class Variable ----@field private variable object +---@field private variable table ---@field private err_handler? function ---@field private _value any ----@field private _poll? object ----@field private _watch? object +---@field private _poll? table +---@field private _watch? table ---@field private poll_interval number ---@field private poll_exec? string[] | string ---@field private poll_transform? fun(next: any, prev: any): any @@ -193,7 +193,7 @@ function Variable:watch(exec, transform) return self end ----@param object object | table[] +---@param object table | table[] ---@param sigOrFn string | fun(...): any ---@param callback fun(...): any ---@return Variable diff --git a/lua/astal/widget.lua b/lua/astal/widget.lua index a10bd1a..7a88a1c 100644 --- a/lua/astal/widget.lua +++ b/lua/astal/widget.lua @@ -3,65 +3,110 @@ local Astal = lgi.require("Astal", "0.1") local Gtk = lgi.require("Gtk", "3.0") local GObject = lgi.require("GObject", "2.0") local Binding = require("astal.binding") +local Variable = require("astal.variable") local exec_async = require("astal.process").exec_async local function filter(tbl, fn) local copy = {} for key, value in pairs(tbl) do if fn(value, key) then - copy[key] = value + if type(key) == "number" then + table.insert(copy, value) + else + copy[key] = value + end + end + end + return copy +end + +local function map(tbl, fn) + local copy = {} + for key, value in pairs(tbl) do + copy[key] = fn(value) + end + return copy +end + +local flatten +flatten = function(tbl) + local copy = {} + for _, value in pairs(tbl) do + if type(value) == "table" and getmetatable(value) == nil then + for _, inner in pairs(flatten(value)) do + table.insert(copy, inner) + end + else + table.insert(copy, value) end end return copy end -local function set_child(parent, child) - if parent.get_child ~= nil then +local function set_children(parent, children) + children = map(flatten(children), function(item) + if Gtk.Widget:is_type_of(item) then + return item + end + return Gtk.Label({ + visible = true, + label = tostring(item), + }) + end) + + -- remove + if Gtk.Bin:is_type_of(parent) then local rm = parent:get_child() if rm ~= nil then parent:remove(rm) end end - if parent.add ~= nil then - parent:add(child) + + -- FIXME: add rest of the edge cases like Stack + if Astal.Box:is_type_of(parent) then + parent:set_children(children) + elseif Astal.CenterBox:is_type_of(parent) then + parent.start_widget = children[1] + parent.center_widget = children[2] + parent.end_widget = children[3] + elseif Astal.Overlay:is_type_of(parent) then + parent:set_child(children[1]) + children[1] = nil + parent:set_overlays(children) + elseif Gtk.Container:is_type_of(parent) then + for _, child in pairs(children) do + if Gtk.Widget:is_type_of(child) then + parent:add(child) + end + end end end -Gtk.Widget._attribute.css = { - get = Astal.widget_get_css, - set = Astal.widget_set_css, -} - -Gtk.Widget._attribute.class_name = { - get = function(self) - local result = "" - local strings = Astal.widget_set_class_names(self) - for i, str in ipairs(strings) do - result = result .. str - if i < #strings then - result = result .. " " +local function merge_bindings(array) + local function get_values() + return map(array, function(v) + if getmetatable(v) == Binding then + return v:get() + else + return v end - end - return result - end, - set = function(self, class_name) - local names = {} - for word in class_name:gmatch("%S+") do - table.insert(names, word) - end - Astal.widget_set_class_names(self, names) - end, -} + end) + end -Gtk.Widget._attribute.cursor = { - get = Astal.widget_get_cursor, - set = Astal.widget_set_cursor, -} + local bindings = filter(array, function(v) + return getmetatable(v) == Binding + end) -Astal.Box._attribute.children = { - get = Astal.Box.get_children, - set = Astal.Box.set_children, -} + if #bindings == 0 then + return array + end + + if #bindings == 1 then + return bindings[1]:as(get_values) + end + + return Variable.derive(bindings, get_values)() +end local function astalify(ctor) function ctor:hook(object, signalOrCallback, callback) @@ -88,24 +133,22 @@ local function astalify(ctor) local bindings = {} local setup = tbl.setup - local visible - if type(tbl.visible) == "boolean" then - visible = tbl.visible - else - visible = true + -- collect children + local children = merge_bindings(flatten(filter(tbl, function(_, key) + return type(key) == "number" + end))) + + -- default visible to true + if type(tbl.visible) ~= "boolean" then + tbl.visible = true end + -- filter props local props = filter(tbl, function(_, key) - return key ~= "visible" and key ~= "setup" + return type(key) == "string" and key ~= "setup" end) - for prop, value in pairs(props) do - if getmetatable(value) == Binding then - bindings[prop] = value - props[prop] = value:get() - end - end - + -- handle on_ handlers that are strings for prop, value in pairs(props) do if string.sub(prop, 0, 2) == "on" and type(value) ~= "function" then props[prop] = function() @@ -114,24 +157,36 @@ local function astalify(ctor) end end + -- handle bindings + for prop, value in pairs(props) do + if getmetatable(value) == Binding then + bindings[prop] = value + props[prop] = value:get() + end + end + + -- construct, attach bindings, add children local widget = ctor(props) for prop, binding in pairs(bindings) do - if prop == "child" then - widget.on_destroy = binding:subscribe(function(v) - set_child(widget, v) - end) - else - widget.on_destroy = binding:subscribe(function(v) - widget[prop] = v - end) - end + widget.on_destroy = binding:subscribe(function(v) + widget[prop] = v + end) + end + + if getmetatable(children) == Binding then + set_children(widget, children:get()) + widget.on_destroy = children:subscribe(function(v) + set_children(widget, v) + end) + else + set_children(widget, children) end - widget.visible = visible if type(setup) == "function" then setup(widget) end + return widget end end @@ -160,6 +215,42 @@ local Widget = { Window = astalify(Astal.Window), } +Gtk.Widget._attribute.css = { + get = Astal.widget_get_css, + set = Astal.widget_set_css, +} + +Gtk.Widget._attribute.class_name = { + get = function(self) + local result = "" + local strings = Astal.widget_set_class_names(self) + for i, str in ipairs(strings) do + result = result .. str + if i < #strings then + result = result .. " " + end + end + return result + end, + set = function(self, class_name) + local names = {} + for word in class_name:gmatch("%S+") do + table.insert(names, word) + end + Astal.widget_set_class_names(self, names) + end, +} + +Gtk.Widget._attribute.cursor = { + get = Astal.widget_get_cursor, + set = Astal.widget_set_cursor, +} + +Astal.Box._attribute.children = { + get = Astal.Box.get_children, + set = Astal.Box.set_children, +} + return setmetatable(Widget, { __call = function(_, ctor) return astalify(ctor) |