z_mirror/flib

git clone https://git.raiguard.me/z_mirror/flib

Caleb HeuerMove custom types under the `flib` namespace2e58ad2

trunk
12.4 KiB358 linesraw
1-- TodoMVC implementation using gui-lite.
2-- The following is how I (raiguard) prefer to structure GUIs, but it is not the only way.
3
4-- GUI
5
6local flib_gui = require("__flib__.gui-lite")
7local mod_gui = require("__core__.lualib.mod-gui")
8
9--- @alias flib_test.TestGuiMode
10--- | "all"
11--- | "active"
12--- | "completed"
13
14--- @class flib_test.TestGui
15--- @field elems table<string, LuaGuiElement>
16--- @field player LuaPlayer
17--- @field completed_count integer
18--- @field items_left integer
19--- @field mode flib_test.TestGuiMode
20--- @field pinned boolean
21
22--- @class flib.TestGuiBase
23local gui = {}
24
25--- @param name string
26--- @param sprite string
27--- @param tooltip LocalisedString
28--- @param handler function
29local function frame_action_button(name, sprite, tooltip, handler)
30  return {
31    type = "sprite-button",
32    name = name,
33    style = "frame_action_button",
34    sprite = sprite .. "_white",
35    hovered_sprite = sprite .. "_black",
36    clicked_sprite = sprite .. "_black",
37    tooltip = tooltip,
38    handler = handler,
39  }
40end
41
42--- Build the GUI for the given player.
43--- @param player LuaPlayer
44function gui.build(player)
45  -- `elems` is a table consisting of all GUI elements that were given names, keyed by their name.
46  -- `window` is the GUI element that was created first, which in this case, was the top-level frame.
47  -- The second argument can be a single element or an array of elements. Here we pass a single element.
48  local elems = flib_gui.add(player.gui.screen, {
49    type = "frame",
50    name = "flib_todo_window",
51    direction = "vertical",
52    -- Use `elem_mods` to make modifications to the GUI element after creation.
53    --- @diagnostic disable-next-line: missing-fields
54    elem_mods = { auto_center = true },
55    -- If `handler` is a function, it will call that function for any GUI event on this element.
56    -- If it is a dictioanry of event -> function, it will call the corresponding function for the corresponding event.
57    handler = { [defines.events.on_gui_closed] = gui.on_window_closed },
58    -- Children can be defined as array members of an element.
59    {
60      type = "flow",
61      style = "flib_titlebar_flow",
62      -- The string must be the name of an element that is present in the `elems` table. To set drag_target to a
63      -- LuaGuiElement reference, do so inside of the `elem_mods` table.
64      drag_target = "flib_todo_window",
65      -- For a real mod, you would want to use localised strings for the captions. They are omitted here to keep this
66      -- demo in one file.
67      { type = "label", style = "frame_title", caption = "TodoMVC", ignored_by_interaction = true },
68      { type = "empty-widget", style = "flib_titlebar_drag_handle", ignored_by_interaction = true },
69      -- You can use helper functions for repetitive elements.
70      frame_action_button("pin_button", "flib_pin", { "gui.flib-keep-open" }, gui.toggle_pinned),
71      frame_action_button("close_button", "utility/close", { "gui.close-instruction" }, gui.hide),
72    },
73    {
74      type = "frame",
75      style = "inside_shallow_frame",
76      -- Use `style_mods` to make modifications to the element's style.
77      --- @diagnostic disable-next-line: missing-fields
78      style_mods = { width = 500 },
79      direction = "vertical",
80      {
81        type = "frame",
82        style = "subheader_frame",
83        {
84          type = "textfield",
85          name = "textfield",
86          style = "flib_widthless_textfield",
87          style_mods = { horizontally_stretchable = true },
88          handler = {
89            -- Multiple different event handlers
90            [defines.events.on_gui_confirmed] = gui.on_textfield_confirmed,
91            [defines.events.on_gui_text_changed] = gui.on_textfield_text_changed,
92          },
93          {
94            type = "label",
95            name = "placeholder",
96            style_mods = { font_color = { a = 0.4 } },
97            caption = "What needs to be done?",
98            ignored_by_interaction = true,
99          },
100        },
101      },
102      {
103        type = "scroll-pane",
104        style = "flib_naked_scroll_pane_no_padding",
105        style_mods = { maximal_height = 400 },
106        -- We use a flow here to allow customizing the vertical spacing with style_mods. Normally you want to use a data
107        -- stage style on the scroll-pane for this.
108        {
109          type = "flow",
110          name = "todos_flow",
111          style_mods = { vertical_spacing = 8, padding = 12 },
112          direction = "vertical",
113        },
114      },
115      {
116        type = "frame",
117        name = "subfooter",
118        style = "subfooter_frame",
119        {
120          type = "flow",
121          style = "centering_horizontal_flow",
122          { type = "label", name = "count_label", style_mods = { left_margin = 8 }, caption = "0 items left" },
123          { type = "empty-widget", style = "flib_horizontal_pusher" },
124          {
125            type = "flow",
126            style_mods = { horizontal_spacing = 8 },
127            {
128              type = "radiobutton",
129              name = "all_radio",
130              caption = "All",
131              state = true,
132              -- Element tags can be specified like this.
133              tags = { mode = "all" },
134              handler = { [defines.events.on_gui_checked_state_changed] = gui.change_mode },
135            },
136            {
137              type = "radiobutton",
138              name = "active_radio",
139              caption = "Active",
140              state = false,
141              tags = { mode = "active" },
142              handler = { [defines.events.on_gui_checked_state_changed] = gui.change_mode },
143            },
144            {
145              type = "radiobutton",
146              name = "completed_radio",
147              caption = "Completed",
148              state = false,
149              tags = { mode = "completed" },
150              handler = { [defines.events.on_gui_checked_state_changed] = gui.change_mode },
151            },
152          },
153          { type = "empty-widget", style = "flib_horizontal_pusher" },
154          {
155            type = "button",
156            name = "clear_completed_button",
157            caption = "Clear completed",
158            enabled = false,
159            -- Because on_gui_click is the only event related to buttons, we can take a shortcut.
160            handler = gui.clear_completed,
161          },
162        },
163      },
164    },
165  })
166
167  -- In a real mod, you would want to initially hide the GUI and not set opened until the player opens it.
168  player.opened = elems.flib_todo_window
169
170  --- @type flib_test.TestGui
171  storage.guis[player.index] = {
172    elems = elems,
173    player = player,
174    -- State variables
175    completed_count = 0,
176    items_left = 0,
177    mode = "all",
178    pinned = false,
179  }
180end
181
182--- @param e EventData.on_gui_confirmed
183function gui.on_textfield_text_changed(_, e)
184  if #e.element.text == 0 then
185    e.element.placeholder.visible = true
186  else
187    e.element.placeholder.visible = false
188  end
189end
190
191--- @param self flib_test.TestGui
192--- @param e EventData.on_gui_checked_state_changed
193function gui.change_mode(self, e)
194  local mode = e.element.tags.mode --[[@as flib_test.TestGuiMode]]
195  self.mode = mode
196  self.elems.all_radio.state = mode == "all"
197  self.elems.active_radio.state = mode == "active"
198  self.elems.completed_radio.state = mode == "completed"
199  -- Adjust checkbox visibility
200  for _, checkbox in pairs(self.elems.todos_flow.children) do
201    checkbox.visible = (checkbox.state and mode ~= "active") or (not checkbox.state and mode ~= "completed")
202  end
203end
204
205--- @param self flib_test.TestGui
206function gui.clear_completed(self)
207  for _, checkbox in pairs(self.elems.todos_flow.children) do
208    if checkbox.state then
209      checkbox.destroy()
210    end
211  end
212  self.completed_count = 0
213  gui.update_footer(self)
214end
215
216--- @param self flib_test.TestGui
217function gui.hide(self)
218  self.elems.flib_todo_window.visible = false
219end
220
221--- @param self flib_test.TestGui
222--- @param e EventData.on_gui_checked_state_changed
223function gui.on_todo_toggled(self, e)
224  local checkbox = e.element
225  if checkbox.state then
226    -- Hide this item if needed
227    if self.mode == "active" then
228      checkbox.visible = false
229    end
230    -- Decrement items left counter
231    self.items_left = self.items_left - 1
232    self.completed_count = self.completed_count + 1
233  else
234    self.items_left = self.items_left + 1
235    self.completed_count = self.completed_count - 1
236  end
237  gui.update_footer(self)
238end
239
240--- @param self flib_test.TestGui
241--- @param e EventData.on_gui_confirmed
242function gui.on_textfield_confirmed(self, e)
243  local title = e.element.text
244  if #title == 0 then
245    self.player.play_sound({ path = "utility/cannot_build" })
246    return
247  end
248  local todos_flow = self.elems.todos_flow
249  flib_gui.add(todos_flow, {
250    type = "checkbox",
251    --- @diagnostic disable-next-line: missing-fields
252    style_mods = { horizontally_stretchable = true },
253    caption = title,
254    state = false,
255    visible = self.mode ~= "completed",
256    handler = { [defines.events.on_gui_checked_state_changed] = gui.on_todo_toggled },
257  })
258  self.items_left = self.items_left + 1
259  e.element.text = ""
260  -- The above line doesn't fire the on_gui_text_changed event, so call its handler manually.
261  -- The event table isn't actually the right type, but it has the same info, so it's fine.
262  gui.on_textfield_text_changed(self, e)
263  gui.update_footer(self)
264end
265
266--- @param self flib_test.TestGui
267function gui.on_window_closed(self)
268  -- Don't close when enabling the pin
269  if self.pinned then
270    return
271  end
272  gui.hide(self)
273end
274
275--- @param self flib_test.TestGui
276function gui.show(self)
277  self.elems.flib_todo_window.visible = true
278  self.elems.textfield.focus()
279  if not self.pinned then
280    self.player.opened = self.elems.flib_todo_window
281  end
282end
283
284--- @param self flib_test.TestGui
285function gui.toggle_pinned(self)
286  -- "Pinning" the GUI will remove it from player.opened, allowing it to coexist with other windows.
287  -- I highly recommend implementing this for your GUIs. flib includes the requisite sprites and locale for the button.
288  self.pinned = not self.pinned
289  if self.pinned then
290    self.elems.close_button.tooltip = { "gui.close" }
291    self.elems.pin_button.sprite = "flib_pin_black"
292    self.elems.pin_button.style = "flib_selected_frame_action_button"
293    if self.player.opened == self.elems.flib_todo_window then
294      self.player.opened = nil
295    end
296  else
297    self.elems.close_button.tooltip = { "gui.close-instruction" }
298    self.elems.pin_button.sprite = "flib_pin_white"
299    self.elems.pin_button.style = "frame_action_button"
300    self.player.opened = self.elems.flib_todo_window
301  end
302end
303
304--- @param self flib_test.TestGui
305function gui.toggle_visible(self)
306  if self.elems.flib_todo_window.visible then
307    gui.hide(self)
308  else
309    gui.show(self)
310  end
311end
312
313--- @param self flib_test.TestGui
314function gui.update_footer(self)
315  self.elems.count_label.caption = self.items_left .. " items left"
316  self.elems.clear_completed_button.enabled = self.completed_count > 0
317end
318
319-- Add all functions in the `gui` table as callable handlers. This is required in order for functions in `gui.add` to
320-- work. For convenience, flib will ignore any value that isn't a function.
321-- The second argument is an optional wrapper function that will be called in lieu of the specified handler of an
322-- element. It is used in this case to get the GUI table for the corresponding player before calling the handler.
323flib_gui.add_handlers(gui, function(e, handler)
324  local self = storage.guis[e.player_index]
325  if self then
326    handler(self, e)
327  end
328end)
329
330-- BOOTSTRAP
331
332-- Handle all 'on_gui_*' events with `flib_gui.dispatch`. If you don't call this, then your element handlers won't work!
333-- If you wish to have custom logic for a specific GUI event, you can call `flib_gui.dispatch` yourself in your main
334-- event handler. `handle_events` will not override any existing event handlers.
335flib_gui.handle_events()
336
337-- Initalize guis table
338script.on_init(function()
339  storage.guis = {}
340end)
341
342-- Create the GUI when a player is created
343script.on_event(defines.events.on_player_created, function(e)
344  local player = game.get_player(e.player_index) --[[@as LuaPlayer]]
345  gui.build(player)
346  -- Add mod_gui button
347  local button_flow = mod_gui.get_button_flow(player) --[[@as LuaGuiElement]]
348  flib_gui.add(button_flow, {
349    type = "button",
350    style = mod_gui.button_style,
351    caption = "TodoMVC",
352    handler = gui.toggle_visible,
353  })
354end)
355
356-- For a real mod, you would also want to handle on_configuration_changed to rebuild your GUIs, and on_player_removed
357-- to remove the GUI table from storage. You would also want to ensure that the GUI is valid before running methods.
358-- For the sake of brevity, these things were not covered in this demo.