Log In  


Hello @zep. I'm writing this thread not really as a bug report, but maybe it is one, I'm not really sure at the moment. I wanted to gather my notes about the current state of the GUI lib (OS 0.1.1d) as I'm trying to deepen my use of it as I'm making my tools and doodles with it.

First, to the other people that might read this thread, I wanted just to say that I'm aware that it's an evolving library like the rest of the OS. Thus, the details discussed here might either stay relevant or fall out of date through time and releases. In the same movement I would like to list the lexicon I'm most likely going to use just so we are on the same tracks.

  • Every time you use gui:attach or gui:new, you create a GuiElement instance, so I'm going to stick with "GUI element" or just "element" to denominate everything attached to the GUI built from those functions.
  • A GUI tree (or sub-tree) is composed of a root element and all the children elements down to the leaves. Usually, the root element created with create_gui is usually a program's GUI tree root. I also tend to call an element's tree what is actually the whole sub-tree composed of that element and its children.
  • GuiElement:event tries to call an element's event handler depending on the event's type (msg.event), so the element's method named like so. For instance, update and draw are two (special) event handlers.
  • I mostly use the term "property" for any element member that isn't a function and has actual usage in the library, such as x, y, width, etc....

So, let's see what I have found so far.

MAYBE BUG The hidden burden of the draw handler

Last seen 0.1.1d

First, I noted that in a GUI tree, if an element doesn't have a draw handler, a few key points of the GUI logic falls short of working on it or its children. For instance, screen clipping doesn't apply on that one element and the hidden property actually stops hiding the element's tree. This quirks shows itself most often when one tries to use the scrollbar element. Here's an example of the issue:

function _init()
	gui = create_gui()

	-- Working version
	do
		local framer = gui:attach{x=0, y=0, width=240, height=262}
		function framer:draw()
			rectfill(0, 0, self.width  - 1, self.height - 1, 5)
		end

		local outer = framer:attach{
			x=8, y=64,
			width=64, height = 96}
		function outer:draw() end

		local inner = outer:attach{
			x=0, y=0,
			width=0, width_rel=1, height=128}

		function inner:draw()
			for y=0,self.height, 16 do
				print(string.format("y : %d", y), 2, y, 7)
			end
		end
		outer:attach_scrollbars()
	end
	-- "Broken" version
	do
		local framer = gui:attach{x=240, y=0, width=240, height=262}
		function framer:draw()
			rectfill(0, 0, self.width  - 1, self.height - 1, 1)
		end

		local outer = framer:attach{
			x=8, y=64,
			width=64, height = 96}

		local inner = outer:attach{
			x=0, y=0,
			width=0, width_rel=1, height=128}

		function inner:draw()
			for y=0,self.height, 16 do
				print(string.format("y : %d", y), 2, y, 7)
			end
		end
		outer:attach_scrollbars()
	end
end

function _update()
	gui:update_all()
end

function _draw()
	cls()
	gui:draw_all()
end

This snippet should create a scrollable list element on a grey background on the left and a scrollable list element on a blue background. Both of those elements have the same size and contains the same inner and scrollbar elements. The main difference here is that the list's outer element (the one that contains the scrollbar) on the left has its own draw handler and thus propagates its own clipping region to its inner element (the scrollable element) and the latter is properly clipped. The list on the right doesn't have a draw handler and thus doesn't clip its region and the list ends up "overflowing", and this even with the clip_to_parent property's default value as true. The GUI tree should look like that:

root
	- grey framer, clips the screen
		- outer element, clips the screen
			- inner element
			- scrollbar
	- blue framer, clips the screen
		- outer element, doesn't clip
			- inner element, clipped by the blue frame's clipping
			- scrollbar

I'm not totally sure whether it's a bug. Granted, clipping might be expensive and using the existence of a draw handler makes sense because it's going to be used in its context, but I end up adding quite a few nop functions to elements to let them properly visually clip, hence why I'm listing this quirk here: I'd like to know if it's indeed an intended behavior and what should be expected to clipping elements or if it's just a side effect. If it's an intended behavior, will there be an alternative solution in the future (like a clipping property)?

In a related quirk, the logic around the hidden property also depends on the presence of the draw handler in an unexpected way: if the draw handler is missing for a hidden element, the hidden property will block the update event from moving down the element's tree's handles but not the draw event, causing the element and its tree to not react to input but still draw. Here's a variant of the precedent screen that shows the issue:

function _init()
	gui = create_gui()

	-- Working version
	do
		local framer = gui:attach{x=0, y=0, width=240, height=262}
		function framer:draw()
			rectfill(0, 0, self.width  - 1, self.height - 1, 5)
		end

		local outer = framer:attach{
			x=8, y=64,
			width=64, height = 96,
			hidden = true}
		function outer:draw() end

		local inner = outer:attach{
			x=0, y=0,
			width=0, width_rel=1, height=128}

		function inner:draw()
			for y=0,self.height, 16 do
				print(string.format("y : %d", y), 2, y, 7)
			end
		end
		outer:attach_scrollbars()
	end
	-- "Broken" version
	do
		local framer = gui:attach{x=240, y=0, width=240, height=262}
		function framer:draw()
			rectfill(0, 0, self.width  - 1, self.height - 1, 1)
		end

		local outer = framer:attach{
			x=8, y=64,
			width=64, height = 96,
			hidden = true}

		local inner = outer:attach{
			x=0, y=0,
			width=0, width_rel=1, height=128}

		function inner:draw()
			for y=0,self.height, 16 do
				print(string.format("y : %d", y), 2, y, 7)
			end
		end
		outer:attach_scrollbars()
	end
end

function _update()
	gui:update_all()
end

function _draw()
	cls()
	gui:draw_all()
end


The difference from the previous snippet is that the list on the left should be invisible where the one on the right side is visible but you can't interact with. Again, I'm not sure if it's an intended behavior. I'd expect instead to have an inhibiting property to halt the event from browsing the tree or having the update handler return true which is already something that looks like thought of and designed when reading the library. Is that going to stay or change in the future?

MAYBE BUG Scrollbar content are clickable past their parents

Last seen 0.1.1d

Another issue involving scrollable elements I'm noticing are that their child elements can end up be interactive past the clipping region of the scrollable element's container. This actually might be closely related to the previous quirk but I'm not fully certain, so I'm going to leave it separate for now.

The way I conceptualize scrollbar elements are elements that convert their parents into a clipping "window" of the scrollable child (the parent's first child element). If they have a draw event handler, the scrollable's draw region will be then clipped by the parent's draw region (see the section about the "hidden burden of the draw handler"), which makes it visibly work fine, but I get unexpected click or tap events from children of the scrollable element where they shouldn't be clickable due to them being out of the parent's region. Let's consider the following tree:

	- root element
		- header element, just to have something with height here.
			- button element, visibly on the header
		- contianer element
			- inner element, a.k.a. the scrollable element
				- some element used as item in a list
				- some element used as item in a list
			- scrollbar element
				...

And consider the following snippet that reflects the given hieararchy:

gui = create_gui()

function _init()
	local header = gui:attach{
		x = 8, y = 8,
		width = 64,
		height = 16,
		draw = function(self, msg)
			rectfill(0, 0, self.width - 1, self.height - 1, 12)
		end
	}
	local container = gui:attach {
		x = 8, y = 24,
		width = 64,
		height = 64,
		draw = function(self, msg)
			rectfill(0, 0, self.width - 1, self.height - 1, 8)
		end
	}
	local scrollable = container:attach {
		x = 0, y = 0,
		width_rel = 1,
		width_add = -8, -- Leaving space for the scrollbar
		height = 128,		
	}

	local i = 1
	for y = 0, scrollable.height, 16 do
		scrollable:attach_button {
			x = 0,
			y = y,
			width_rel = 1,
			height = 16,
			message = string.format("Button %d pressed", i),
			label = string.format("Button %d", i),
			tap = function(self, msg)
				notify(self.message)
			end
		}
		i += 1
	end

	container:attach_scrollbars()
end

function _update()
	gui:update_all()
end

function _draw()
	cls()
	gui:draw_all()
end

If the scrollable element's y is at zero (so, effectively no scrolling applied), the user will be able to tap on the item through the region as expected. Yet, as soon as enough scrolling is done, the user can still click on the now out of frame list item elements by clicking them where the header is. The behavior can be confirmed visually by disabling parent clipping with the clip_to_parent property set to false: the items will draw over the header element and tapping on them will work. That is shown when running the snippet where you can press the first buttons of the list through the blue banner if you scroll down enough. Interestingly enough, any element past the clipping region's bottom border won't register input events.

What issues could this cause is that the header's button element can be effectively covered by the scrollable element and if the scrollable's elements were scrolled up enough to have an interactive element on top of said button, they'll "steal" the event. The result is that an item made invisible by being clipped out by the parenting hierarchy will still process the input event as if not clipped.

So far, even though clip_to_parent is set by default to true, I found no real "clean" solution with the GUI's API to prevent this and I suspect this might be a bug related to how the clipping region is passed from parent to child in el_at_xy_recursive but I have to test that on my side before anymore finger pointing at the library. In the meantime, can you confirm that listening to even out of the drawing region with clip_to_parent set to true is an intended behavior or a bug?

While iterating over this post's draft I noticed three more quirks: if I move the header element's creation after the container's, the header will prevent the contained buttons from being clicked. Also, it looks like the container's clipping region only affects the scrollable element, not its children, which would explain somewhat the issue. And the cherry on the top: the header element can be removed and the same issue will still appear.

Addendum: There's a slight tweak you seem to have done as an optimization in el_at_xy_recursive, the if (true or is_inside or not el.clip_to_parent) part at line 691. If I turn true into false or remove it, the problem disappears and the clipping works as intended. Is that a regression from previous versions? I don't remember such problem appearing before.

Addendum 2: removing the shortcut is actually faster in one of my GUI-heavy projects than keeping it. I'm not sure if that'd be a global improvement but that was funny to see such drastic change (20% when idling in a specific region down to 16% post fix)

MAYBE BUG Nested scrollbar regions

Last seen 0.1.1d

I noticed that nested scrollbar regions don't consume scrolling events, potentially leaving scenarios where you scroll in two regions at once if they're nested. There might be a problem with the scrollbar elements themselves too where they're not always properly sized or clipped. Consider the following snippet:

gui = create_gui()

function _init()
	local outer_container = gui:attach {
		x = 0, y = 0,
		width_rel = 0.5,
		height = 128,

		draw = function(self)
			rectfill(0, 0, self.width - 1, self.height - 1, 1)
		end
	}
	local inner_container = outer_container:attach{
			x = 0, y = 0,
			width_rel = 0.5,
			height = 256,

			draw = function(self)
				rectfill(0, 0, self.width - 1, self.height - 1, 2)
			end
	}	

	local scrollable = inner_container:attach {
			x = 0, y = 0,
			width_rel = 0.5,
			height = 512,
			draw = function(self)
				rectfill(0, 0, self.width - 1, self.height - 1, 7)
			end	
	}
	inner_container:attach_scrollbars()
	outer_container:attach_scrollbars()
end

function _update()
	gui:update_all()
end

function _draw()
	cls()
	gui:draw_all()
end

In the screenshot, you can see a white scrollable element inside another scrollable element which also is nested into another element. We end up with two scrollbars as expected but if one scrolls in the white region, they'll end up scrolling in both regions. You can also clearly see that the inner scrollbar goes past the associated scrollable element and its parent, which might not be expected.

From what I understand, input events are processed from the deepest element up to the GUI as long as an event handler doesn't return true. Scrollbar elements process the mousewheel event but do not prevent the event from bubbling up, which allows their parent to also process and eventually scroll too. I'm not sure if it's intended or a bug, should they inhibit the event instead? Regarding the scrollbar's wrong size, I don't have a clue about what could be changed yet.

QUESTION Extending the GuiElement class

Last seen 0.1.1d

This item is not really a bug report but closer to a design question to follow your library's philosophy. Because GUI elements come with a lot of nifty bits here and there such as clipping, automatic camera calls and event handlers, I tend to make a lot of small elements like composite items in scrollable lists but the way I'm usually doing involves creating the same table over and over where a metatable would lighten the memory (and CPU for initialization) usage. Given that GUI elements already depend on a metatable (GuiElement, the one never to be used outside gui.lua) and that it's being set in both new and attach (by the calling the former), if I were to try implementing another class, what would be the proper way of handling the change of metatable if attach would just remove it? I'd like to overwrite the smallest amount of properties or methods from the base class in order to be forward-compatible. The closest I ever was something like this:

-- Just a helper function to clone a table. I clone the metatables and extend them in my class inheritance system
function deep_copy(t, dest, a_type)
	t = t or {}
	local r = dest or {}
	for k,v in pairs(t) do
		if a_type ~= nil and type(v) == a_type then
			r[k] = (type(v) == 'table')
				and ((_classes[v]) and v or deep_copy(v))
				or v
		elseif a_type == nil then
			r[k] = (type(v) == 'table') 
				and k~= '__index' and ((_classes[v]) and v or deep_copy(v)) 
				or v
		end
	end
	return r
end

--[[ 
	My variant of the class system. I forgot where I started from.
	What's in the mt member is inherited by copy.
]]
function new(base, ...)
	local e = {}
	print(base.name)
	setmetatable(e, base.mt)
	e.class = base
	e:init(...)
	return e
end
_classes = {}
function class(name, base)
	local cls = {
		new = new,
		mt = {}
	}
	if base then
		deep_copy (base.mt, cls.mt)
		cls.mt.super = base.mt
	end
	cls.name = name
	cls.mt.__index = cls.mt
	_classes[name] = cls
	return cls
end

gui = create_gui()

-- [[Creating a wrapper proto-class to base new classes from it. This class might actually be irrelevant but I haven't tried without it for now.]]
local base = getmetatable(gui)
local base_ctor = function(el) return gui:new(el) end
local GuiElementWrapper = {mt = {}}
do
	local cls = GuiElementWrapper.mt
	function cls:new(el)
		local el = base_ctor(el)
		return setmetatable(el, {
			__index = function(t, k)
			return self.mt[k] or base[k]
			end
		})
	end

	function cls:init()
		print "GuiElementWrapper:init"
	end

end

Foo = class ("Foo", GuiElementWrapper)
do
	local cls = Foo.mt
	function cls:init()
		self.super:init()
		print "Foo:init"
	end

	function cls:do_something()
		print("I did something")
	end
end

test = gui:attach(Foo:new())
-- Sadly, we lose the metatable
print(getmetatable(test) == getmetatable(gui))
-- Uncomment this line to see that we actually lost the binding to the class metatable
-- test:do_something()

The used class system is mostly irrelevant, I could have tried with 30log instead or another class library, but the idea of extending GuiElement with a custom class stays the central point of the question. Sadly, as mentioned at the end of the snippet, as long as we call gui:attach, the element's metatable gets overriden with GuiElement, rendering any attempt at using the inheritance moot unless I alter GuiElement.attach, like in the following snippet:

do
	local prev_attach = gui.attach
	function gui:attach(el)
		local mt = getmetatable(el)
		local res = prev_attach(self, el)
		if (mt) setmetatable(res, mt)
		return res
	end
end

If done before creating the test element, the metatable will be preserved and test:do_something() will actually do something but I'm not fully satisfied with this solution because it does replace something directly in GuiElement, potientially causing subtle issues in the future, at least, not the ones involving trying to cram an Object-oriented design into the library's usage. Is replacing gui.attach okay like this? Is there going to be a "cleaner" or at least official way in the future to extended the GUI library without hot-swapping methods or the metatable on the fly like this?

That's all I have for now, I don't know if I'll do many of those posts where I gather many issues at once. I'll try to tend to the post with more findings about those quirks at the very least. Anyway, I hope you'll have a nice day!




[Please log in to post a comment]