diff options
author | Aylur <[email protected]> | 2024-06-09 22:25:40 +0200 |
---|---|---|
committer | Aylur <[email protected]> | 2024-06-09 22:25:40 +0200 |
commit | 41cb376a02d12f85eb1e4893425af15614c2e187 (patch) | |
tree | e5d684784c4a3782adbbaad7e6597d3dafd8bb6f | |
parent | 15285a17bf447c5185dfbb92d9a4bd2670a4e44e (diff) |
support deeply nested layout structures
js, jsx, lua now allows deeply nested layouts that contains Bindings
Variable.derive will be automatically constructed where needed
and Bindings in layouts will be bound automatically
it breaks compatibility with ags even more, but jsx should be preferred
imo anyway
-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) |