z_mirror/flib
git clone https://git.raiguard.me/z_mirror/flib
2e58ad2
trunk
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 ) 30return { 31type ="sprite-button" , 32name =name , 33style ="frame_action_button" , 34sprite =sprite .. "_white" , 35hovered_sprite =sprite .. "_black" , 36clicked_sprite =sprite .. "_black" , 37tooltip =tooltip , 38handler =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. 48local elems =flib_gui . add ( player . gui . screen , { 49type ="frame" , 50name ="flib_todo_window" , 51direction ="vertical" , 52-- Use `elem_mods` to make modifications to the GUI element after creation. 53--- @diagnostic disable-next-line: missing-fields 54elem_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. 57handler ={ [ defines . events . on_gui_closed ] =gui . on_window_closed } , 58-- Children can be defined as array members of an element. 59{ 60type ="flow" , 61style ="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. 64drag_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. 70frame_action_button ( "pin_button" , "flib_pin" , { "gui.flib-keep-open" } , gui . toggle_pinned ), 71frame_action_button ( "close_button" , "utility/close" , { "gui.close-instruction" } , gui . hide ), 72} , 73{ 74type ="frame" , 75style ="inside_shallow_frame" , 76-- Use `style_mods` to make modifications to the element's style. 77--- @diagnostic disable-next-line: missing-fields 78style_mods ={ width =500 } , 79direction ="vertical" , 80{ 81type ="frame" , 82style ="subheader_frame" , 83{ 84type ="textfield" , 85name ="textfield" , 86style ="flib_widthless_textfield" , 87style_mods ={ horizontally_stretchable =true } , 88handler ={ 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{ 94type ="label" , 95name ="placeholder" , 96style_mods ={ font_color ={ a =0.4 } } , 97caption ="What needs to be done?" , 98ignored_by_interaction =true , 99} , 100} , 101} , 102{ 103type ="scroll-pane" , 104style ="flib_naked_scroll_pane_no_padding" , 105style_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{ 109type ="flow" , 110name ="todos_flow" , 111style_mods ={ vertical_spacing =8 , padding =12 } , 112direction ="vertical" , 113} , 114} , 115{ 116type ="frame" , 117name ="subfooter" , 118style ="subfooter_frame" , 119{ 120type ="flow" , 121style ="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{ 125type ="flow" , 126style_mods ={ horizontal_spacing =8 } , 127{ 128type ="radiobutton" , 129name ="all_radio" , 130caption ="All" , 131state =true , 132-- Element tags can be specified like this. 133tags ={ mode ="all" } , 134handler ={ [ defines . events . on_gui_checked_state_changed ] =gui . change_mode } , 135} , 136{ 137type ="radiobutton" , 138name ="active_radio" , 139caption ="Active" , 140state =false , 141tags ={ mode ="active" } , 142handler ={ [ defines . events . on_gui_checked_state_changed ] =gui . change_mode } , 143} , 144{ 145type ="radiobutton" , 146name ="completed_radio" , 147caption ="Completed" , 148state =false , 149tags ={ mode ="completed" } , 150handler ={ [ defines . events . on_gui_checked_state_changed ] =gui . change_mode } , 151} , 152} , 153{ type ="empty-widget" , style ="flib_horizontal_pusher" } , 154{ 155type ="button" , 156name ="clear_completed_button" , 157caption ="Clear completed" , 158enabled =false , 159-- Because on_gui_click is the only event related to buttons, we can take a shortcut. 160handler =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. 168player . opened =elems . flib_todo_window 169 170--- @type flib_test.TestGui 171storage . guis [ player . index ] ={ 172elems =elems , 173player =player , 174-- State variables 175completed_count =0 , 176items_left =0 , 177mode ="all" , 178pinned =false , 179} 180end 181 182--- @param e EventData.on_gui_confirmed 183function gui . on_textfield_text_changed ( _ , e ) 184if # e . element . text == 0 then 185e . element . placeholder . visible =true 186else 187e . element . placeholder . visible =false 188end 189end 190 191--- @param self flib_test.TestGui 192--- @param e EventData.on_gui_checked_state_changed 193function gui . change_mode ( self , e ) 194local mode =e . element . tags . mode --[[@as flib_test.TestGuiMode]] 195self . mode =mode 196self . elems . all_radio . state =mode == "all" 197self . elems . active_radio . state =mode == "active" 198self . elems . completed_radio . state =mode == "completed" 199-- Adjust checkbox visibility 200for _ , checkbox in pairs ( self . elems . todos_flow . children ) do 201checkbox . visible =( checkbox . state and mode ~= "active" ) or ( not checkbox . state and mode ~= "completed" ) 202end 203end 204 205--- @param self flib_test.TestGui 206function gui . clear_completed ( self ) 207for _ , checkbox in pairs ( self . elems . todos_flow . children ) do 208if checkbox . state then 209checkbox . destroy () 210end 211end 212self . completed_count =0 213gui . update_footer ( self ) 214end 215 216--- @param self flib_test.TestGui 217function gui . hide ( self ) 218self . 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 ) 224local checkbox =e . element 225if checkbox . state then 226-- Hide this item if needed 227if self . mode == "active" then 228checkbox . visible =false 229end 230-- Decrement items left counter 231self . items_left =self . items_left - 1 232self . completed_count =self . completed_count + 1 233else 234self . items_left =self . items_left + 1 235self . completed_count =self . completed_count - 1 236end 237gui . 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 ) 243local title =e . element . text 244if # title == 0 then 245self . player . play_sound ( { path ="utility/cannot_build" } ) 246return 247end 248local todos_flow =self . elems . todos_flow 249flib_gui . add ( todos_flow , { 250type ="checkbox" , 251--- @diagnostic disable-next-line: missing-fields 252style_mods ={ horizontally_stretchable =true } , 253caption =title , 254state =false , 255visible =self . mode ~= "completed" , 256handler ={ [ defines . events . on_gui_checked_state_changed ] =gui . on_todo_toggled } , 257} ) 258self . items_left =self . items_left + 1 259e . 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. 262gui . on_textfield_text_changed ( self , e ) 263gui . 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 269if self . pinned then 270return 271end 272gui . hide ( self ) 273end 274 275--- @param self flib_test.TestGui 276function gui . show ( self ) 277self . elems . flib_todo_window . visible =true 278self . elems . textfield . focus () 279if not self . pinned then 280self . player . opened =self . elems . flib_todo_window 281end 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. 288self . pinned =not self . pinned 289if self . pinned then 290self . elems . close_button . tooltip ={ "gui.close" } 291self . elems . pin_button . sprite ="flib_pin_black" 292self . elems . pin_button . style ="flib_selected_frame_action_button" 293if self . player . opened == self . elems . flib_todo_window then 294self . player . opened =nil 295end 296else 297self . elems . close_button . tooltip ={ "gui.close-instruction" } 298self . elems . pin_button . sprite ="flib_pin_white" 299self . elems . pin_button . style ="frame_action_button" 300self . player . opened =self . elems . flib_todo_window 301end 302end 303 304--- @param self flib_test.TestGui 305function gui . toggle_visible ( self ) 306if self . elems . flib_todo_window . visible then 307gui . hide ( self ) 308else 309gui . show ( self ) 310end 311end 312 313--- @param self flib_test.TestGui 314function gui . update_footer ( self ) 315self . elems . count_label . caption =self . items_left .. " items left" 316self . 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 ) 324local self =storage . guis [ e . player_index ] 325if self then 326handler ( self , e ) 327end 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 () 339storage . guis ={} 340end ) 341 342-- Create the GUI when a player is created 343script . on_event ( defines . events . on_player_created , function ( e ) 344local player =game . get_player ( e . player_index ) --[[@as LuaPlayer]] 345gui . build ( player ) 346-- Add mod_gui button 347local button_flow =mod_gui . get_button_flow ( player ) --[[@as LuaGuiElement]] 348flib_gui . add ( button_flow , { 349type ="button" , 350style =mod_gui . button_style , 351caption ="TodoMVC" , 352handler =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.