-
Notifications
You must be signed in to change notification settings - Fork 49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improvements to the GUI Framework #13
Comments
I decided to play around a bit with this. Ended up making a special event system for GUI events. Take a look and see what you think. Still working on it but so far so good. This is the branch with my WIP code https://github.com/credomane/FactorioMods_Stdlib/tree/guiupdates Mostly duplicated the Event code to Gui.Event. Then refactored (abused) that to support per gui_element_pattern handlers per event. |
Looks like a solid start. When I wrote the beginnings of the GUI module, there was only one GUI event, but Factorio 0.13 added a few more, so a completely separate event registry makes sense. I'm curious if you have any other suggestions for improvements. The pattern support for events is a small improvement, but dealing with GUI elements still sucks; I haven't come up with any workable alternatives either. |
Personally, I find making the GUI's in Factorio not that bad. Nothing like dealing with the freaking giant monolithic function the event handlers become instantly. Had an very rough idea how to solve it for my mod but I came looking to stdlib for an easier way first. Found there wasn't one but looking at the Event code I instantly knew how to combine my rough idea with your Event module. The GUI event wrapper/subsystem/thing I've added has made dealing with GUI elements events super easy. Almost jQuery easy, IMO. I believe that Here is a bunch of sample code and snippets for you or anyone that likes such things for how to use something. I know I do. game.player[1].gui.top.add({type="checkbox", name="mytestelement", state = true, caption = "test"})
function testhandler(event)
Game.print_all(event.name .. " state is now " .. tostring(event.state))
end
--All of these work and at the same time too.
Gui.on_checked_state_changed("mytestelement", testhandler)
Gui.on_checked_state_changed("mytest", anotherhandler)
Gui.on_checked_state_changed("my", function(event) doSomething.tm() end)
--Replacing a handler is as simple as
Gui.on_checked_state_changed("my", newHandler)
--As I was typing this sample code I realized I should allow removing a handler like this so I added it.
Gui.on_checked_state_changed("mytest", nil)
Gui.on_checked_state_changed("my", nil)
--Support for any new defines.events.on_gui_* [and current] events can be immediately done like this
-- until a proper Gui.on_* helper can be created.
--Example new event on_gui_dragged
Gui.Event.register(defines.events.on_gui_dragged, "gui_element_pattern", myDraggedHandler);
Gui.Event.remove(defines.events.on_gui_dragged, "gui_element_pattern");
--if for some reason the "fake" event is missing something (future factorio version additions) then use event._event to get the real original event. [edit] Also as far as I can tell Gui.on_click() should be fully backwards compatible with the previous implementation. |
Some quick ideas for easing GUI creation...I think jQuery (again for my inspiration) style search would be nice, perhaps? Even possible or too cumbersome for Lua and/or Factorio's implementation? I'm still way too used to WoW's Lua implementation and API. Factorio devs could probably take some things from WoW from a UI API standpoint. While WoW's was far from perfect the UI API was fairly powerful once you figured it out. Here's a theoretical snippet of my Gui idea. --Returns a special Gui table with a list of found elements. Searches gui.left, gui.top, gui.center by default
Gui.find_elements({name="myElement"}) -- returns elements matching name exactly. (regExp support?)
Gui.find_elements({position=LuaElement}) -- find ALL elements under LuaElement even children's children! Should probably have a better name than position.
Gui.find_elements({parent=LuaElement}) -- return only direct children of LuaElement
Gui.find_elements({type="button"}) -- return all buttons
--Mix-n-match
Gui.find_elements({type="button",parent=LuaElement}) -- return all buttons with specified parent
Gui.find_elements({type="button",parent="myElement"}) -- return all buttons with a parent named "myElement"
--Passing a plain string is internally changed to {name=parameter_value}
Gui.find_elements("myElement")
--Perhaps overload Gui to Gui.find_elements()?
Gui({name="myElement"})
--Add a child element to each element found
Gui({name="myElement"}).add("{factorio luaElement table info here"})
--Destroy all the found elements
Gui({name="myElement"}).destroy()
--Create an event handler for all the found elements
Gui({name="myElement"}).on_click(myHandler)
[edit] |
Actually if you want to model things on web frameworks (not a bad idea at all), might I recommend the Elm style? It handles monolithic GUI's (in compiles to javascript) via HTML by a very succinct and easy to handle and expand subscription/event system. It is declarative instead of imperative so it is much harder to break things accidentally as you use it. |
I'm open to anything. My intended goal for the Gui mock-up isn't a full featured gui manager but a ease-of-use wrapper to the underlining Factorio API. From my understanding the mock-up I provided is declarative, right? The mock up being the gist I provided in a edit. The find_elements is nice idea but I provided a poor design for it, I think. As for the elm style? What is that? Don't think I've ever used that. |
Elm is one of those languages that compiles to Javascript, been around 3 or 4 years though has recently reached 1.0 and has gotten very popular lately, it is kind of a much easier to use Haskell, strongly typed, with its compiler doing a lot of optimizations on the code. One of those languages where if it compiles you can be sure it works. Ignore the Elm language though and look at its concepts where were the same model to make things like React and Redux (the high performance and crash-proof libraries that facebook released a few years ago for javascript). In a high level it essentially works like this (in Lua'ese, the concepts are important here, not the language): There are 2 data concepts:
There are 4 callbacks:
And Commands are used to send a command to the outside system (or even to send another message internally). And of course it could be registered like: StdLib.Program{
init = init,
view = view,
update = update,
subscriptions = subscriptions,
} So that is the overall external interface for it. An entire mod could be written in such a way and it would be performant, smaller, and easier to write without bugs (and with some debug-time-enabled checks the back-end could verify the front-end state for various things to catch mis-types and such), but even for just a GUI it is a great format that scales well. Normal apps have view calls that branch to other functions based on various criteria in model, can loop to make lists of things, can filter the data easily, the subscriptions are even dynamic and can be enabled/disabled as necessary (the backend would do the work of registering or unregistering with Factorio as the events vanish or not from one subscription state to the other). This is the style that Elm, React/Redux/Flow, mercury(sp?) and many others take on Javascript that is the fastest updating of any other frameworks. The state management (even without the view callback) is also fantastic for stability of a program due to translation of data from one state to the next. |
The Factorio GUI API is very small and rigid. Build anything other than an almost one to one API to API easy-to-use wrapper seems like a lot of effort for little return. I've tried using react.js in the past and found it to be nothing but frustratingly difficult to use. I just didn't 'get' it; I still don't. Found jQuery shortly there after and never looked back. Your sample code reminds me very much of reactjs. So assuming that, if I was to undertake making an elm-style gui lib for stdlib I'd quite poorly as I can't even understand the elm model in the first place. Not saying my jquery style would be great either. Just that if I was to make both a jquery and reactjs gui lib then the jquery style would come out infinitely better. This is @Afforess' project so we'd have wait for a decision on this anyway. If the decision is for elm-like I'd take a crack at it at least. Both as developer and a user of stdlib. |
I thoroughly enjoyed following this discussion and intentionally didn't respond immediately so as to not interrupt the brainstorming. My initial wish would be that Factorio gui system look like something sane. Even if the Factorio GUI API was modeled after something like JS/CSS that would be a huge improvement. Unfortunately, it isn't. I've come at the problem from a lot of angles, even really extreme ones where stdlib replaces the The main intractable issue I can't see a solution past is the one of closures and persistence. I don't see the Elm model, or any other existing frameworks I am aware of bridging this issue. The problem, as I see it, is that you can't create GUI event handlers dynamically (for example, at the same time you create a GUI object), because:
I can't see past this. The web has no analogous situation, because when a web page is created, its not persisted, and even if you decide to save the html, css, and js that all composes a webpage, when you re-open it, your browser re-executes it from an empty initial state. It does not persist the page content.* Everything you've written so far seems solid. One minor suggestion I have is that *localstorage, browser cookies are not content. |
Extreme indeed. I'm laughing so hard at the ridiculousness of it and yet, I'm seriously considering the feasibility of going through with it. A mad/evil scientist type laugh. I need a minute....ok. All better. I would think the only way to really do it and do it properly would be to "demand" that I never thought about the event handler persistence in Factorio with any stdlib api style the gui portion becomes. That does pose an interesting issue. Do we register an internal on_load handler to re-register missing event handlers? So then the mod dev can just trust that the event listener will always be there until they specifically remove it? That is the type of thing that factorio's mod_configuration_changed event is meant to handle I would say. As long as it was made clear in the docs that stdlib registered events would persist across game loads I think it would be fine. Also, what do were do if we do have an internal on_load but we are fighting the stdlib user over it because they are, by their rightful choice, manually doing Currently my way of doing it (read: likely the wrong way) is to have a function that is called by on_init and on_load. This function registers all of my GUI handlers. The way I have the gui event system working only one handler per pattern. So multiple registers of the same pattern only use the most recently registered handler and due to factorio's design if the gui can't be seen then it doesn't actually exist so the gui event subsystem will never see the pattern I told it to listen for. So having them floating around "permanently" without the gui element is ok. My way poses an issue if I did things like
Ok. I'll issue a pull request then. This is for the Gui.Event subsystem only...that I kinda already started using in my "refactor-in-progress" mod too. I haven't even started on a Gui.find_elements it was just and idea I threw out there. Figured once the gui api proper came into being it would naturally be molded into that. ...This took a surprising amount of time to type. I saw your post when it was 38 seconds old....now it is just an hour old. [edit] |
Replacing the game object is a lot easier than it sounds. The reason I have not done that is not the difficulty, or questionable nature, but the maintenance cost. The maintenance cost of replacing/hot-patching the existing game API is huge, and will make upgrading from Factorio 0.13 to 0.14 and beyond much more challenging. So replacing the existing game API is always an option, but an option I prefer to leave in the deck unless all else fails.
How exactly do you see mod upgrades/migrations working? Does a modder have to explicitly write migration code? How complex is it to update the event handlers? I think it's worth remembering the Factorio modder community is largely novice programmers, and many have a lot of trouble with the existing prototype migration included in base Factorio... anything more complicated than that will be too difficult for any sort of practical adoption, or worse, will hurt stdlib as confused modders write buggy code. I'm open to persisting handlers, but tread lightly. I think its critical this remains intuitive. |
That is where I ended up too, in a way it would be the Factorio version of updating forge for new Minecraft releases. Minus the obfuscation hurdles, of course.
They would... Would be something like this striped down version from my mod. I'm not arguing for it. Just how it would be done. The more I think about it the less I like the idea of attempting it. --on_configuration_changed()
function mod.configuration_changed(event)
for modName,modTable in pairs(event.mod_changes) do
if modName == MOD_FULLNAME then
mod.doMigration(modTable.old_version, modTable.new_version);
end
end
end
function mod.doMigration(oldVersion, newVersion)
print_all({"Updater", "Version changed from " .. oldVersion .. " to " .. newVersion .. "."});
if oldVersion > newVersion then
print_all({"Updater", "Version downgrade detected. I can't believe you've done this."});
return;
end
if oldVersion < "0.0.7" then --or whatever version the handlers were changed in.
--remove old handlers here.
end
I was considering a Gui.Event.purge() which would unceremoniously remove all gui events but I'm not convinced of the necessity for it. Then you would just register what you wanted. |
The Elm style is indeed a bit... inverted from what most people are used to, but once you get it (with a proper example instead of a long description it is substantially more obvious) it makes a lot more sense. :-)
Actually that is the interesting thing about this style, even in javascript Elm does not register event handlers on the objects but rather it registers a global handler when they bubble-up through the DOM. Elm (and react) can run server-side as well, and that affects its style for a few reasons (described shortly). In Factorio it would work very well because:
This fits this model precisely. The
The handlers should absolutely not ever be persisted in such a system, ever ever ever. This style (and Elm thanks to this style) can migrate versions very easily, like Elm can even migrate from version-to-version without ever restarting (live code updates!). The 'version_change' is just another message given to Just remember, only the model/state is serialized, that is completely user controlled, not even the back-end system should store anything in state, it is a stateless system other than the completely user-controlled Let's try a simple mod in this style, how about something that hurts all players every time any craft anything in their inventory with it hurting more and more each (encourages machine crafting) (too simple of an example for this, but to show the concepts): -- GLOBALS CONFIG START
-- Low enough to encourage no buildings, high enough to give a good buffer before reaching better assembler tech
local harm_per_item = 0.5 -- LUA really needs a 'const' type...
-- GLOBALS CONFIG END
StdLib = require("./stdlib/stdlib")
Cmd = require("./stdlib/cmd")
Sub = require("./stdlib/sub")
local init = function()
return { itemscrafted = 0 } -- I like to make a wrapping table so it is easy to extend later if the need arises
end
local handlers = { -- I like to put my handlers in a table and index into it instead of doing if blah then elseif chains
on_player_crafted_item = function(msg, model)
model.itemscrafted += msg.item_stack.count
return model, Cmd.Factorio.AllPlayers.Damage(model.itemscrafted * harm_per_item)
end
}
local update = function(msg, model)
return handlers[msg.id] -- technically if msg.id does not exist then I have a bug as all cases that I register for should be handled, so this naive version is fine, even for release
end
local subscriptions = function(model)
return Sub.Factorio.Events("on_player_crafted_item")
end
StdLib.Mod{
model = global,
init = init,
update = update,
subscriptions = subscriptions
} So the first time the mod loads (there is an empty 'global' for example) then the 'Mod' runner runs init to populate the global. The game could stop at this point for as much as anything cares and that would be saved out. In the backend, probably 'on_load' or so, then call ( At this point since subscriptions should not mutate state then everything is still serializable out. The player soon crafts an item, thus the backend event handler that was registered for "on_player_crafted_item" gets called, it then calls It is a system that is designed to scale arbitrarily, easily, and safely, so it would be an especially huge boon to mods with lots of script functionality, especially like GUI mods. In a Factorio setup you could probably get rid of the whole It is easy to extend this system as well, for example someone could make a Cmd that can run things a certain amount of ticks in the future, or on a pattern, and if no ticks are happening it could unregister "on_tick", it could be something like this: -- File: stdlib/cmd/timer.lua
StdLib = require("./stdlib/stdlib")
Cmd = require("./stdlib/cmd")
sub = require("./stdlib/sub")
local MSG_ONESHOT = "oneshot"
local init = function()
return { oneshots = {} }
end
local update = function(msg, model)
if msg.id == "on_tick" then -- Keep on_ticks simple!
local at_time_list = model.oneshot[game.tick] or []
if #at_time_list > 0 then
local cmds = []
for i = 1, #at_time_list do
cmds[#cmds+1] = Cmd.msg(msg)
end
model.oneshot[game.tick] = nil
return model, Cmd.batch(cmds)
else
return model, Cmd.none
end
else if msg.id == MSG_ONESHOT then
if msg.in_ticks <= 0 then
return model, Cmd.msg(msg) -- Registered in the past? Run it now then!
end
local at_time = game.tick + msg.in_ticks
local at_time_list = model.oneshot[at_time] or []
at_time_list[#at_time_list+1] = msg
model.oneshot[at_time] = at_time_list
return model, Cmd.none
end
end
local subscriptions = function(model)
if #model.oneshots > 0 then
return Sub.Factorio.Events("on_tick")
else
return Sub.none
end
end
return {
init = init,
update = update,
subscriptions = subscriptions,
Cmds = {
Oneshot = function(in_ticks, msg) return Cmd.msg{id=MSG_NAME, msg={id=MSG_ONESHOT, in_ticks=in_ticks, msg=msg}}
}
} Which could be used in the prior mod to, say, delay damage for one second by doing: -- GLOBALS CONFIG START
-- Low enough to encourage no buildings, high enough to give a good buffer before reaching better assembler tech
local harm_per_item = 0.5 -- LUA really needs a 'const' type...
local harm_delay_ticks = 60
-- GLOBALS CONFIG END
StdLib = require("./stdlib/stdlib")
Cmd = require("./stdlib/cmd")
Sub = require("./stdlib/sub")
Timer = require("./stdlib/timer")
local init = function()
return {
itemscrafted = 0,
timer = Timer.init() -- Explicit is better than implicit!
} -- I like to make a wrapping table so it is easy to extend later if the need arises
end
local handlers = { -- I like to put my handlers in a table and index into it instead of doing if blah then elseif chains
on_player_crafted_item = function(msg, model)
model.itemscrafted += msg.item_stack.count
return model, Timer.Cmds.Oneshot(harm_delay_ticks, Cmd.Factorio.AllPlayers.Damage(model.itemscrafted * harm_per_item))
end
timer = function(msg, model) -- Explicit about what is handled, implicit is *bad*
return Timer.update(msg.timer, model.timer)
end
}
local update = function(msg, model)
return handlers[msg.id] -- technically if msg.id does not exist then I have a bug as all cases that I register for should be handled, so this naive version is fine, even for release
end
local subscriptions = function(model)
return Sub.batch{
Sub.Factorio.Events("on_player_crafted_item"),
Sub.map{sub=Timer.subscriptions(model.timer), id="timer", msg={id="timer"}} -- `map` takes a subscription and 'maps' it into another message, in this case any `msg` for timer will be wrapped with `{id="timer", timer=TimerMsg}`, you could do the same via your own function handler, this is just a more simple method.
}
end
StdLib.Mod{
model = global,
init = init,
update = update,
subscriptions = subscriptions
} And badda boom we have an automatic 60 second delay to harming the player after crafting and it will register and unregister the on_tick as it is needed or not (as the Timer subscription function only asks for it when there are timers waiting). We can add in more This style is extremely powerful and every module in this style in the other languages follow this same format. All state is encapsulated and restricted to the model, even importing modules that require state require explicitely storing their state so you know exactly what is happening, where, and when. Do note, I'm not necessarily pushing for this, rather I hope that the design can help push for a better GUI system (whatever that may be, and perhaps even a global modding style) in this library as it is highly useful. |
Could we use a HAML-ish DSL (let's call it FUI) for defining gui structure, and then a jquery-ish syntax (let's call it fuiQuery) for interacting with the gui? FUI example local myGui = [[
dialog#name .title='title'
vertical#options
checkbox#foo .label='some text'
]] Just a very rough example to give a jist of what it might look like. The benefits of this approach are:
|
Hey there, I've just stumbled over this library on the forums and I would like to share a Gui implementation I've done myself For now it bases on lets say 'Gui pages' gui.append
{
type = "button",
name = "dytech-debug-button",
open = "dytech-debug-flow",
parent = "dytech-menu",
caption = { "dytech-gui.debug-button" },
}
gui.create
{
type = "flow",
name = "dytech-debug-flow",
direction = "horizontal",
childs =
{
{
type = "frame",
name = "dytech-debug-menu",
parent = "dytech-menu",
direction = "vertical",
caption = { "dytech-gui.debug-title" },
childs =
{
{
type = "button",
name = "debug-research-all-button",
caption = { "dytech-gui.debug-research-all-button" }
},
{
type = "button",
name = "debug-toggle-cheats-button",
caption = { "dytech-gui.debug-toggle-cheats-button" }
},
{
type = "button",
name = "debug-enable-all-button",
caption = { "dytech-gui.debug-enable-all-button" }
},
}
},
}
} When it comes to event handling you can do this by just declaring a new function upon a special field / table function gui.clicked.debug_research_all_button(event)
local player_force = game.players[event.player_index].force
player_force.research_all_technologies()
end
function gui.clicked.debug_toggle_cheats_button(event)
game.players[event.player_index].cheat_mode = not game.players[event.player_index].cheat_mode
end To make all this work you would need to call the gui.handle_gui_event function (during the gui event, obviously) However besides that I've also added special events (but only for elements in that system) refresh and loaded --
-- Sets the caption on the chunk pollution label
function core.gui.refresh.chunk_pollution(event)
local pollution = 0
local chunk_num = 0
for chunk in game.surfaces.nauvis.get_chunks() do
local tile_x = chunk.x * 32 + 16
local tile_y = chunk.y * 32 + 16
chunk_num = chunk_num + 1
pollution = pollution + game.surfaces.nauvis.get_pollution { tile_x, tile_y }
end
-- Set the caption
event.element.caption = { "dytech-gui.core-stats-chunk-pollution", string.format("%.2f", pollution / chunk_num) }
end To make use of these events you would also need to call the gui.handle_time_events during the on_tick event. Alongside this you can set a "default" gui page that will be displayed on the beginning and / or after some timeout has passed (this also needs to call the 'gui.handle_time_events' function) And we got also simple configuration values with it -- Idle timeout (after which the gui will reset to the base state: control.lua@196)
-- * if you set '0' it will disable the gui reset
gui.idle_time = 600
gui.idle_timeout = { } -- Each player has it's own
-- Refresh timeout (after what time should elements get a 'refresh' event)
gui.refresh_time = 180
gui.refresh_timeout = 0 This system is used in my attempt to restore "DyTech" mods, you can get a glimpse on the gui system here: https://github.com/Dandielo/CORE-DyTech-Core/tree/master/scripts It's quite good but needs still a but of tinkering and some changes as because not all events are handled yet |
The GUI is a fertile ground for stdlib. Some broad suggestions include:
Forum Suggestions: https://forums.factorio.com/viewtopic.php?f=96&t=23181&p=158743#p158740
Significant effort needs to be invested in making icon styles much easier to use, and in general making the gui api less opaque and difficult to learn or maintain.
The text was updated successfully, but these errors were encountered: