diff --git a/README.md b/README.md index d1d4c91..ab199e2 100644 --- a/README.md +++ b/README.md @@ -26,16 +26,21 @@ The Gooey system is encapsulated in a single Lua module without any visual compo ## Input bindings For Gooey to work it requires a couple of input bindings: -* Mouse trigger - ```mouse-button-1``` -> ```touch``` -* Mouse trigger - ```mouse-wheel-up``` -> ```scroll_up``` (for scrolling in lists) -* Mouse trigger - ```mouse-wheel-down``` -> ```scroll_down``` (for scrolling in lists) -* Key trigger - ```key-backspace``` -> ```backspace``` (for text input) -* Text trigger - ```text``` -> ```text``` (for text input) -* Text trigger - ```marked-text``` -> ```marked_text``` (for text input) - +* Mouse trigger - `mouse-button-1` -> `touch` +* Mouse trigger - `mouse-wheel-up` -> `scroll_up` (for scrolling in lists) +* Mouse trigger - `mouse-wheel-down` -> `scroll_down` (for scrolling in lists) +* Key trigger - `key-backspace` -> `backspace` (for text input) +* Text trigger - `text` -> `text` (for text input) +* Text trigger - `marked-text` -> `marked_text` (for text input) ![](docs/key_mouse_bindings.png) ![](docs/text_bindings.png) +Optional additional keybindings when working with component groups: + +* `next` - Focus the next component in a component group +* `previous` - Focus the previous component in a component group +* `select` - Alternative to clicking. Will click/select the currently focused component + The input binding constants listed above are defined in `gooey/actions.lua` and can be changed. **IMPORTANT NOTE ON ANDROID**: Make sure that the Input Method in the Android section of the game.project file is set to HiddenInputField. This will ensure that virtual keyboard input works properly. @@ -43,316 +48,334 @@ The input binding constants listed above are defined in `gooey/actions.lua` and ### Multi-touch Gooey supports multi-touch for clickable components as long as the following input binding exists: -* Touch trigger - ```multi-touch``` -> ```multitouch``` +* Touch trigger - `multi-touch` -> `multitouch` + + +## Components +Gooey components are defined as a component function and a component state. A component function typically accept arguments in this order: + +1. Id - a unique component id, usually matching a gui node id +2. Arguments - zero or more component specific arguments +3. Action id - the on_input action id +4. Action - the on_input action table +5. Interact callback - function to call when the component has been interacted with (button click, checkbox toggle, list item selection, etc) +6. Refresh callback - function to call when the component state has been changed (mouse over, mouse out, press, release etc) +```lua +local state = gooey.component(id, arg1, arg2, action_id, action, cb, refresh) +``` + +The state of each component is represented internally as Lua table. Some of the state values are shared between all components while some state values are unique to a component type. All components have the following state values: + + +* `over` (boolean) - true if user action is inside the node +* `over_now` (boolean) - true if user action moved inside the node this call +* `out_now` (boolean) - true if user action moved outside the node this call +* `pressed` (boolean) - true if the component is pressed +* `pressed_now` (boolean) - true if the component was pressed this call +* `released_now` (boolean) - true if the component was released this call +* `long_pressed` (boolean) - true if the registered press was a long press or not +* `clicked` (boolean) - true if the input is considered a click (ie pressed and released cycle) +* `consumed` (boolean) - true if the input was consumed -## Supported components Gooey supports the following component types: -* [Button](#gooeybuttonnode_id-action_id-action-fn-refresh_fn) - ```gooey.button()``` -* [Checkbox](#gooeycheckboxnode_id-action_id-action-fn-refresh_fn) - ```gooey.checkbox()``` -* [Radio button](#gooeyradionode_id-group-action_id-action-fn-refresh_fn) - ```gooey.radio()``` -* [Input text](#gooeyinputnode_id-keyboard_type-action_id-action-config-refresh_fn) - ```gooey.input()``` +* [Button](#gooeybuttonnode_id-action_id-action-fn-refresh_fn) - `gooey.button()` +* [Checkbox](#gooeycheckboxnode_id-action_id-action-fn-refresh_fn) - `gooey.checkbox()` +* [Radio button](#gooeyradionode_id-group-action_id-action-fn-refresh_fn) - `gooey.radio()` +* [Input text](#gooeyinputnode_id-keyboard_type-action_id-action-config-refresh_fn) - `gooey.input()` * Lists ([static](#gooeystatic_listlist_id-stencil_id-item_ids-action_id-action-fn-refresh_fn) and [dynamic](#gooeydynamic_listlist_id-root_id-stencil_id-item_id-data-action_id-action-fn-refresh_fn)): - * ```gooey.static_list()``` All list item nodes are already added to the list. Good for showing a small data set or when the list item nodes should vary in composition and looks. - * ```gooey.dynamic_list()``` All list item nodes are created from the same template. The nodes are reused when scrolling. Good for showing a large data set. -* [Vertical scrollbar](#gooeyvertical_scrollbarhandle_id-bounds_id-action_id-action-fn-refresh_fn) - ```gooey.scrollbar()``` + * `gooey.static_list()` All list item nodes are already added to the list. Good for showing a small data set or when the list item nodes should vary in composition and looks. + * `gooey.dynamic_list()` All list item nodes are created from the same template. The nodes are reused when scrolling. Good for showing a large data set. +* [Vertical scrollbar](#gooeyvertical_scrollbarhandle_id-bounds_id-action_id-action-fn-refresh_fn) - `gooey.scrollbar()` +* Group - `gooey.group()` ### gooey.button(node_id, action_id, action, fn, refresh_fn) Perform input and state handling for a button **PARAMETERS** -* ```node_id``` (string|hash) - Id of the node representing the clickable area, typically the background of the button -* ```action_id``` (hash) - Action id as received from on_input() -* ```action``` (table) - Action as received from on_input() -* ```fn``` (function) - Function to call when the button is clicked/tapped on. A button is considered clicked/tapped if both a pressed and released action has been detected inside the bounds of the node. The function will get the same state table as described below passed as its first argument -* ```refresh_fn``` (function) - Optional function to call when the state of the button has been updated. Use this to update the visual representation. +* `node_id` (string|hash) - Id of the node representing the clickable area, typically the background of the button +* `action_id` (hash) - Action id as received from on_input() +* `action` (table) - Action as received from on_input() +* `fn` (function) - Function to call when the button is clicked/tapped on. A button is considered clicked/tapped if both a pressed and released action has been detected inside the bounds of the node. The function will get the same state table as described below passed as its first argument +* `refresh_fn` (function) - Optional function to call when the state of the button has been updated. Use this to update the visual representation. **RETURN** -* ```button``` (table) - State data for the button based on current and previous input actions +* `button` (table) - State data for the button based on current and previous input actions The state table contains the following fields: -* ```node``` (node) - The node itself -* ```node_id``` (node_id) - Hashed id of the node -* ```enabled``` (boolean) - true if the node is enabled -* ```consumed``` (boolean) - true if the input was consumed -* ```clicked``` (boolean) - true if the input is considered a click (ie pressed and released cycle) -* ```over``` (boolean) - true if user action is inside the node -* ```over_now``` (boolean) - true if user action moved inside the node this call -* ```out_now``` (boolean) - true if user action moved outside the node this call -* ```pressed``` (boolean) - true if the button is pressed -* ```pressed_now``` (boolean) - true if the button was pressed this call -* ```long_pressed``` (boolean) - true if the registered press was a long press or not -* ```released_now``` (boolean) - true if the button was released this call +* `node` (node) - The node itself +* `node_id` (node_id) - Hashed id of the node +* `enabled` (boolean) - true if the node is enabled **EXAMPLE** - local gooey = require "gooey.gooey" - - local function update_button(button) - if button.pressed_now then - gui.play_flipbook(button.node, hash("button_pressed")) - elseif button.released_now then - gui.play_flipbook(button.node, hash("button_normal")) - elseif not button.pressed and button.over_now then - gui.play_flipbook(button.node, hash("button_over")) - elseif not button.pressed and button.out_now then - gui.play_flipbook(button.node, hash("button_normal")) - end +```lua +local gooey = require "gooey.gooey" + +local function update_button(button) + if button.pressed_now then + gui.play_flipbook(button.node, hash("button_pressed")) + elseif button.released_now then + gui.play_flipbook(button.node, hash("button_normal")) + elseif not button.pressed and button.over_now then + gui.play_flipbook(button.node, hash("button_over")) + elseif not button.pressed and button.out_now then + gui.play_flipbook(button.node, hash("button_normal")) end +end - local function on_pressed(button) - print("pressed") - end +local function on_pressed(button) + print("pressed") +end - function on_input(self, action_id, action) - gooey.button("button/bg", action_id, action, on_pressed, update_button) - end +function on_input(self, action_id, action) + gooey.button("button/bg", action_id, action, on_pressed, update_button) +end +``` **STATE** It is possible to set the state of a button: - update_button(gooey.button("button/bg").set_visible(false)) +```lua +update_button(gooey.button("button/bg").set_visible(false)) +``` **CONFIG** It is possible to configure the minimum time required to detect a long-press: - gooey.button("button/bg").set_long_pressed_time(time) +```lua +gooey.button("button/bg").set_long_pressed_time(time) +``` ### gooey.checkbox(node_id, action_id, action, fn, refresh_fn) Perform input and state handling for a checkbox **PARAMETERS** -* ```node_id``` (string|hash) - Id of the node representing the clickable area -* ```action_id``` (hash) - Action id as received from on_input() -* ```action``` (table) - Action as received from on_input() -* ```fn``` (function) - Function to call when the checkbox is checked/unchecked on. A checkbox is considered checked/unchecked if both a pressed and released action has been detected inside the bounds of the node. The function will get the same state table as described below passed as its first argument -* ```refresh_fn``` (function) - Optional function to call when the state of the checkbox has been updated. Use this to update the visual representation. +* `node_id` (string|hash) - Id of the node representing the clickable area +* `action_id` (hash) - Action id as received from on_input() +* `action` (table) - Action as received from on_input() +* `fn` (function) - Function to call when the checkbox is checked/unchecked. A checkbox is considered checked/unchecked if both a pressed and released action has been detected inside the bounds of the node. The function will get the same state table as described below passed as its first argument +* `refresh_fn` (function) - Optional function to call when the state of the checkbox has been updated. Use this to update the visual representation. **RETURN** -* ```checkbox``` (table) - State data for the checkbox based on current and previous input actions +* `checkbox` (table) - State data for the checkbox based on current and previous input actions The state table contains the following fields: -* ```node``` (node) - The node itself -* ```node_id``` (node_id) - Hashed id of the node -* ```enabled``` (boolean) - true if the node is enabled -* ```consumed``` (boolean) - true if the input was consumed -* ```clicked``` (boolean) - true if the input is considered a click (ie pressed and released cycle) -* ```over``` (boolean) - true if user action is inside the node -* ```over_now``` (boolean) - true if user action moved inside the node this call -* ```out_now``` (boolean) - true if user action moved outside the node this call -* ```checked``` (boolean) - The checkbox state (checked/unchecked) -* ```pressed``` (boolean) - true if the checkbox is pressed (ie mouse/touch down but not yet released) -* ```pressed_now``` (boolean) - true if the checkbox was pressed this call -* ```long_pressed``` (boolean) - true if the registered press was a long press or not -* ```released_now``` (boolean) - true if the checkbox was released this call -* ```checked_now``` (boolean) - true if the checkbox was checked this call -* ```unchecked_now``` (boolean) - true if the checkbox was unchecked this call +* `node` (node) - The node itself +* `node_id` (node_id) - Hashed id of the node +* `enabled` (boolean) - true if the node is enabled +* `checked` (boolean) - The checkbox state (checked/unchecked) +* `checked_now` (boolean) - true if the checkbox was checked this call +* `unchecked_now` (boolean) - true if the checkbox was unchecked this call **EXAMPLE** - local gooey = require "gooey.gooey" - - local function update_checkbox(checkbox) - if checkbox.released_now then - if checkbox.checked then - gui.play_flipbook(checkbox.node, hash("checkbox_checked")) - else - gui.play_flipbook(checkbox.node, hash("checkbox_unchecked")) - end - elseif not checkbox.pressed and checkbox.over_now then - gui.play_flipbook(checkbox.node, hash("checkbox_over")) - elseif not checkbox.pressed and checkbox.out_now then - gui.play_flipbook(checkbox.node, hash("checkbox_normal")) +```lua +local gooey = require "gooey.gooey" + +local function update_checkbox(checkbox) + if checkbox.released_now then + if checkbox.checked then + gui.play_flipbook(checkbox.node, hash("checkbox_checked")) + else + gui.play_flipbook(checkbox.node, hash("checkbox_unchecked")) end + elseif not checkbox.pressed and checkbox.over_now then + gui.play_flipbook(checkbox.node, hash("checkbox_over")) + elseif not checkbox.pressed and checkbox.out_now then + gui.play_flipbook(checkbox.node, hash("checkbox_normal")) end +end - local function on_checked(checkbox) - print("checked", checkbox.checked) - end +local function on_checked(checkbox) + print("checked", checkbox.checked) +end - function on_input(self, action_id, action) - gooey.checkbox("checkbox/bg", action_id, action, on_checked, update_checkbox) - end +function on_input(self, action_id, action) + gooey.checkbox("checkbox/bg", action_id, action, on_checked, update_checkbox) +end +``` **STATE** It is possible to set the state of a checkbox. This is good for setting the initial state of the checkbox: - update_checkbox(gooey.checkbox("checkbox/bg").set_checked(true)) - update_checkbox(gooey.checkbox("checkbox/bg").set_visible(false)) +```lua +update_checkbox(gooey.checkbox("checkbox/bg").set_checked(true)) +update_checkbox(gooey.checkbox("checkbox/bg").set_visible(false)) +``` **CONFIG** It is possible to configure the minimum time required to detect a long-press: - gooey.checkbox("checkbox/bg").set_long_pressed_time(time) - +```lua +gooey.checkbox("checkbox/bg").set_long_pressed_time(time) +``` ### gooey.radio(node_id, group, action_id, action, fn, refresh_fn) Perform input and state handling for a radio button **PARAMETERS** -* ```node_id``` (string|hash) - Id of the node representing the clickable area -* ```group``` (string) - Id of the radio group this radio button belongs to (see example) -* ```action_id``` (hash) - Action id as received from on_input() -* ```action``` (table) - Action as received from on_input() -* ```fn``` (function) - Function to call when the radio button is selected. A radio button is considered selected if both a pressed and released action has been detected inside the bounds of the node. The function will get the same state table as described below passed as its first argument -* ```refresh_fn``` (function) - Optional function to call when the state of the radiobutton has been updated. Use this to update the visual representation. +* `node_id` (string|hash) - Id of the node representing the clickable area +* `group` (string) - Id of the radio group this radio button belongs to (see example) +* `action_id` (hash) - Action id as received from on_input() +* `action` (table) - Action as received from on_input() +* `fn` (function) - Function to call when the radio button is selected. A radio button is considered selected if both a pressed and released action has been detected inside the bounds of the node. The function will get the same state table as described below passed as its first argument +* `refresh_fn` (function) - Optional function to call when the state of the radiobutton has been updated. Use this to update the visual representation. **RETURN** -* ```radio``` (table) - State data for the radio button based on current and previous input actions +* `radio` (table) - State data for the radio button based on current and previous input actions The state table contains the following fields: -* ```node``` (node) - The node itself -* ```node_id``` (node_id) - Hashed id of the node -* ```enabled``` (boolean) - true if the node is enabled -* ```consumed``` (boolean) - true if the input was consumed -* ```clicked``` (boolean) - true if the input is considered a click (ie pressed and released cycle) -* ```over``` (boolean) - true if user action is inside the node -* ```over_now``` (boolean) - true if user action moved inside the node this call -* ```out_now``` (boolean) - true if user action moved outside the node this call -* ```selected``` (boolean) - The radio button state -* ```pressed``` (boolean) - true if the radio button is pressed (ie mouse/touch down but not yet released) -* ```pressed_now``` (boolean) - true if the radio button was pressed this call -* ```long_pressed``` (boolean) - true if the registered press was a long press or not -* ```released_now``` (boolean) - true if the radio button was released this call -* ```selected_now``` (boolean) - true if the radio button was selected this call -* ```deselected_now``` (boolean) - true if the radio button was deselected this call +* `node` (node) - The node itself +* `node_id` (node_id) - Hashed id of the node +* `enabled` (boolean) - true if the node is enabled +* `selected` (boolean) - The radio button state +* `selected_now` (boolean) - true if the radio button was selected this call +* `deselected_now` (boolean) - true if the radio button was deselected this call **EXAMPLE** - local gooey = require "gooey.gooey" - - local function update_radio(radio) - if radio.released_now then - if radio.selected then - gui.play_flipbook(radio.node, hash("radio_selected")) - else - gui.play_flipbook(radio.node, hash("radio_normal")) - end - elseif not radio.pressed and radio.over_now then - gui.play_flipbook(radio.node, hash("radio_over")) - elseif not radio.pressed and radio.out_now then +```lua +local gooey = require "gooey.gooey" + +local function update_radio(radio) + if radio.released_now then + if radio.selected then + gui.play_flipbook(radio.node, hash("radio_selected")) + else gui.play_flipbook(radio.node, hash("radio_normal")) end + elseif not radio.pressed and radio.over_now then + gui.play_flipbook(radio.node, hash("radio_over")) + elseif not radio.pressed and radio.out_now then + gui.play_flipbook(radio.node, hash("radio_normal")) end - - function on_input(self, action_id, action) - gooey.radiogroup("MYGROUP", action_id, action, function(group_id, action_id, action) - gooey.radio("radio1/bg", group_id, action_id, action, function(radio) - print("selected 1", radio.selected) - end, update_radio) - gooey.radio("radio2/bg", group_id, action_id, action, function(radio) - print("selected 2", radio.selected) - end), update_radio) - end) - end +end + +function on_input(self, action_id, action) + gooey.radiogroup("MYGROUP", action_id, action, function(group_id, action_id, action) + gooey.radio("radio1/bg", group_id, action_id, action, function(radio) + print("selected 1", radio.selected) + end, update_radio) + gooey.radio("radio2/bg", group_id, action_id, action, function(radio) + print("selected 2", radio.selected) + end), update_radio) + end) +end +``` **STATE** It is possible to set the state of a radiobutton. This is good for setting the initial state of the radiobutton: - update_radio(gooey.radio("radio1/bg").set_selected(true)) - update_radio(gooey.radio("radio1/bg").set_visible(false)) +```lua +update_radio(gooey.radio("radio1/bg").set_selected(true)) +update_radio(gooey.radio("radio1/bg").set_visible(false)) +``` **CONFIG** It is possible to configure the minimum time required to detect a long-press: - gooey.radio("radio1/bg").set_long_pressed_time(time) - +```lua +gooey.radio("radio1/bg").set_long_pressed_time(time) +``` ### gooey.static_list(list_id, stencil_id, item_ids, action_id, action, config, fn, refresh_fn, is_horizontal) Perform input and state handling for a list of items where the list of nodes has already been created. **PARAMETERS** -* ```list_id``` (string) - Id of the template containing the list nodes. -* ```stencil_id``` (string|hash) - Id of the stencil node that is used to clip the list. Touch events outside this area will be ignored when it comes to picking of list items. -* ```item_ids``` (table) - Table with a list of list item ids (hash|string) -* ```action_id``` (hash) - Action id as received from on_input() -* ```action``` (table) - Action as received from on_input() -* ```config``` (table) - Optional table with list configuration -* ```fn``` (function) - Function to call when a list item is selected. A list item is considered selected if both a pressed and released action has been detected inside the bounds of the item. The function will get the same state table as described below passed as its first argument -* ```refresh_fn``` (function) - Optional function to call when the state of the list has been updated. Use this to update the visual representation. -* ```is_horizontal``` (bool) - Optional flag - if true, the list will be handled as horizontal, otherwise - as vertical. +* `list_id` (string) - Id of the template containing the list nodes. +* `stencil_id` (string|hash) - Id of the stencil node that is used to clip the list. Touch events outside this area will be ignored when it comes to picking of list items. +* `item_ids` (table) - Table with a list of list item ids (hash|string) +* `action_id` (hash) - Action id as received from on_input() +* `action` (table) - Action as received from on_input() +* `config` (table) - Optional table with list configuration +* `fn` (function) - Function to call when a list item is selected. A list item is considered selected if both a pressed and released action has been detected inside the bounds of the item. The function will get the same state table as described below passed as its first argument +* `refresh_fn` (function) - Optional function to call when the state of the list has been updated. Use this to update the visual representation. +* `is_horizontal` (bool) - Optional flag - if true, the list will be handled as horizontal, otherwise - as vertical. The `config` table can contain the following values: * `horizontal` (boolean) - The table is in horizontal mode **RETURN** -* ```list``` (table) - State data for the list based on current and previous input actions - -The ```list``` table contains the following fields: - -* ```id``` (string) - The ```list_id``` parameter above -* ```enabled``` (boolean) - true if the node is enabled -* ```consumed``` (boolean) - true if the input was consumed -* ```items``` (table) - The list items as nodes. See below for table structure. -* ```over``` (boolean) - true if user action is over any list item -* ```over_item``` (table) - The list item the user action is over -* ```over_item_now``` (table) - The list item the user action moved inside this call -* ```out_item_now``` (table) - The list item the user action moved outside this call -* ```selected_item``` (table) - Index of the selected list item -* ```pressed_item``` (table) - Index of the pressed list item (ie mouse/touch down but not yet released) -* ```pressed_item_now``` (table) - The list item the user action pressed this call -* ```long_pressed``` (boolean) - true if the registered press was a long press or not -* ```released_item_now``` (table) - The list item the user action released this call -* ```scroll``` (vector3) - Scrolled amount from the top (only scroll.y is used). The scroll amount is in the range 0.0 (top) to 1.0 (bottom). -* ```is_horizontal``` (bool) - Optional flag - if true, the list will be handled as horizontal, otherwise - as vertical. - -The ```items``` table contains list items, each with the following fields: - -* ```root``` (node) - The root GUI node of the list item -* ```nodes``` (table) - Node id to GUI node mappings (as returned from gui.clone_tree) -* ```data``` (any) - The data associated with this list item -* ```index``` (number) - Index of the list item +* `list` (table) - State data for the list based on current and previous input actions + +The `list` table contains the following fields: + +* `id` (string) - The `list_id` parameter above +* `enabled` (boolean) - true if the node is enabled +* `items` (table) - The list items as nodes. See below for table structure. +* `over_item` (table) - The list item the user action is over +* `over_item_now` (table) - The list item the user action moved inside this call +* `out_item_now` (table) - The list item the user action moved outside this call +* `selected_item` (table) - Index of the selected list item +* `pressed_item` (table) - Index of the pressed list item (ie mouse/touch down but not yet released) +* `pressed_item_now` (table) - The list item the user action pressed this call +* `released_item_now` (table) - The list item the user action released this call +* `scroll` (vector3) - Scrolled amount from the top (only scroll.y is used). The scroll amount is in the range 0.0 (top) to 1.0 (bottom). +* `is_horizontal` (bool) - Optional flag - if true, the list will be handled as horizontal, otherwise - as vertical. + +The `items` table contains list items, each with the following fields: + +* `root` (node) - The root GUI node of the list item +* `nodes` (table) - Node id to GUI node mappings (as returned from gui.clone_tree) +* `data` (any) - The data associated with this list item +* `index` (number) - Index of the list item **EXAMPLE** - local gooey = require "gooey.gooey" - - local function update_list(list) - for i,item in ipairs(list.items) do - if item == list.pressed_item then - gui.play_flipbook(item.root, hash("item_pressed")) - elseif item == list.selected_item then - gui.play_flipbook(item.root, hash("item_selected")) - else - gui.play_flipbook(item.root, hash("item_normal")) - end +```lua +local gooey = require "gooey.gooey" + +local function update_list(list) + for i,item in ipairs(list.items) do + if item == list.pressed_item then + gui.play_flipbook(item.root, hash("item_pressed")) + elseif item == list.selected_item then + gui.play_flipbook(item.root, hash("item_selected")) + else + gui.play_flipbook(item.root, hash("item_normal")) end end +end - local function on_item_selected(list) - print("selected", list.selected_item.index) - end +local function on_item_selected(list) + print("selected", list.selected_item.index) +end - function on_input(self, action_id, action) - gooey.static_list("list", "list/stencil", { "item1/bg", "item2/bg", "item3/bg", "item4/bg", "item5/bg" }, action_id, action, nil, on_item_selected, update_list) - end +function on_input(self, action_id, action) + gooey.static_list("list", "list/stencil", { "item1/bg", "item2/bg", "item3/bg", "item4/bg", "item5/bg" }, action_id, action, nil, on_item_selected, update_list) +end +``` **STATE** It is possible to set the scroll amount of a list. This is useful when updating a list based on the movement of a scrollbar: - -- scroll 75% of the way - gooey.static_list("list", "list/stencil", { "item1/bg", "item2/bg", "item3/bg", "item4/bg", "item5/bg" }).scroll_to(0, 0.75) +```lua +-- scroll 75% of the way +gooey.static_list("list", "list/stencil", { "item1/bg", "item2/bg", "item3/bg", "item4/bg", "item5/bg" }).scroll_to(0, 0.75) +``` **CONFIG** It is possible to configure the minimum time required to detect a long-press: - gooey.static_list("list").set_long_pressed_time(time) - +```lua +gooey.static_list("list").set_long_pressed_time(time) +``` ### gooey.dynamic_list(list_id, root_id, stencil_id, item_id, data, action_id, action, config, fn, refresh_fn, is_horizontal) Perform input and state handling for a list of items where list item nodes are created dynamically and reused. This is preferred for large data sets. @@ -360,16 +383,16 @@ Perform input and state handling for a list of items where list item nodes are c NOTE: The list does not support a stencil node with adjust mode set to `gui.ADJUST_STRETCH`. If the list size needs to change it has to be done using `gui.set_size()` on the stencil node. **PARAMETERS** -* ```list_id``` (string) - Id of the template containing the list nodes. -* ```stencil_id``` (string|hash) - Id of the stencil node that is used to clip the list. Touch events outside this area will be ignored when it comes to picking of list items. The size of this area will decide how many list item nodes to create. The system will create enough to fill the area plus one more to support scrolling. -* ```item_id``` (string|hash) - Id of the single list item that is to be cloned to present the list data. -* ```data``` (table) - Data to associate with the list. This decides how far the list of possible to scroll. -* ```action_id``` (hash) - Action id as received from on_input() -* ```action``` (table) - Action as received from on_input() -* ```config``` (table) - Optional table with list configuration -* ```fn``` (function) - Function to call when a list item is selected. A list item is considered selected if both a pressed and released action has been detected inside the bounds of the item. The function will get the same state table as described below passed as its first argument. Return true in this function if the data has changed and the list perform an additional update of the visible nodes. -* ```refresh_fn``` (function) - Optional function to call when the state of the list has been updated. Use this to update the visual representation. -* ```is_horizontal``` (bool) - Optional flag - if true, the list will be handled as horizontal, otherwise - as vertical. +* `list_id` (string) - Id of the template containing the list nodes. +* `stencil_id` (string|hash) - Id of the stencil node that is used to clip the list. Touch events outside this area will be ignored when it comes to picking of list items. The size of this area will decide how many list item nodes to create. The system will create enough to fill the area plus one more to support scrolling. +* `item_id` (string|hash) - Id of the single list item that is to be cloned to present the list data. +* `data` (table) - Data to associate with the list. This decides how far the list of possible to scroll. +* `action_id` (hash) - Action id as received from on_input() +* `action` (table) - Action as received from on_input() +* `config` (table) - Optional table with list configuration +* `fn` (function) - Function to call when a list item is selected. A list item is considered selected if both a pressed and released action has been detected inside the bounds of the item. The function will get the same state table as described below passed as its first argument. Return true in this function if the data has changed and the list perform an additional update of the visible nodes. +* `refresh_fn` (function) - Optional function to call when the state of the list has been updated. Use this to update the visual representation. +* `is_horizontal` (bool) - Optional flag - if true, the list will be handled as horizontal, otherwise - as vertical. The `config` table can contain the following values: @@ -377,63 +400,66 @@ The `config` table can contain the following values: * `carousel` (boolean) - The table is in carousel mode, wrapping around content at the ends **RETURN** -* ```list``` (table) - State data for the list based on current and previous input actions - -The ```list``` table contains the following fields: - -* ```id``` (string) - The ```list_id``` parameter above -* ```enabled``` (boolean) - true if the node is enabled -* ```consumed``` (boolean) - true if the input was consumed -* ```items``` (table) - The list items as nodes. See below for table structure. -* ```over``` (boolean) - true if user action is over any list item -* ```over_item``` (table) - The list item the user action is over -* ```over_item_now``` (table) - The list item the user action moved inside this call -* ```out_item_now``` (table) - The list item the user action moved outside this call -* ```selected_item``` (table) - Index of the selected list item -* ```pressed_item``` (table) - Index of the pressed list item (ie mouse/touch down but not yet released) -* ```pressed_item_now``` (table) - The list item the user action pressed this call -* ```long_pressed``` (boolean) - true if the registered press was a long press or not -* ```released_item_now``` (table) - The list item the user action released this call -* ```scroll``` (vector3) - Scrolled amount from the top (only scroll.y is used). The scroll amount is in the range 0.0 (top) to 1.0 (bottom). -* ```is_horizontal``` (bool) - Optional flag - if true, the list will be handled as horizontal, otherwise - as vertical. - -The ```items``` table contains list items, each with the following fields: - -* ```root``` (node) - The root GUI node of the list item -* ```nodes``` (table) - Node id to GUI node mappings (as returned from gui.clone_tree) -* ```data``` (any) - The data associated with this list item -* ```index``` (number) - Index of the list item +* `list` (table) - State data for the list based on current and previous input actions + +The `list` table contains the following fields: + +* `id` (string) - The `list_id` parameter above +* `enabled` (boolean) - true if the node is enabled +* `items` (table) - The list items as nodes. See below for table structure. +* `over_item` (table) - The list item the user action is over +* `over_item_now` (table) - The list item the user action moved inside this call +* `out_item_now` (table) - The list item the user action moved outside this call +* `selected_item` (table) - Index of the selected list item +* `pressed_item` (table) - Index of the pressed list item (ie mouse/touch down but not yet released) +* `pressed_item_now` (table) - The list item the user action pressed this call +* `released_item_now` (table) - The list item the user action released this call +* `scroll` (vector3) - Scrolled amount from the top (only scroll.y is used). The scroll amount is in the range 0.0 (top) to 1.0 (bottom). +* `is_horizontal` (bool) - Optional flag - if true, the list will be handled as horizontal, otherwise - as vertical. + +The `items` table contains list items, each with the following fields: + +* `root` (node) - The root GUI node of the list item +* `nodes` (table) - Node id to GUI node mappings (as returned from gui.clone_tree) +* `data` (any) - The data associated with this list item +* `index` (number) - Index of the list item **EXAMPLE** - local gooey = require "gooey.gooey" +```lua +local gooey = require "gooey.gooey" - local function update_list(list) - for i,item in ipairs(list.items) do - gui.set_text(item.nodes[hash("listitem/text")], item.data) - end +local function update_list(list) + for i,item in ipairs(list.items) do + gui.set_text(item.nodes[hash("listitem/text")], item.data) end +end - local function on_item_selected(list) - print("selected", list.selected_item.index) - end +local function on_item_selected(list) + print("selected", list.selected_item.index) +end - function on_input(self, action_id, action) - gooey.dynamic_list("list", "list/stencil", "listitem/bg", { "Mr. White", "Mr. Pink", "Mr. Green", "Mr. Blue", "Mr. Yellow" }, action_id, action, nil, on_item_selected, update_list) - end +function on_input(self, action_id, action) + gooey.dynamic_list("list", "list/stencil", "listitem/bg", { "Mr. White", "Mr. Pink", "Mr. Green", "Mr. Blue", "Mr. Yellow" }, action_id, action, nil, on_item_selected, update_list) +end +``` **STATE** It is possible to set the scroll amount of a list. This is useful when updating a list based on the movement of a scrollbar: - -- scroll 75% of the way - gooey.dynamic_list("list", "list/stencil", "listitem/bg", { "Mr. White", "Mr. Pink", "Mr. Green", "Mr. Blue", "Mr. Yellow" }).scroll_to(0, 0.75) +```lua +-- scroll 75% of the way +gooey.dynamic_list("list", "list/stencil", "listitem/bg", { "Mr. White", "Mr. Pink", "Mr. Green", "Mr. Blue", "Mr. Yellow" }).scroll_to(0, 0.75) +``` **CONFIG** It is possible to configure the minimum time required to detect a long-press: - gooey.dynamic_list("list").set_long_pressed_time(time) +```lua +gooey.dynamic_list("list").set_long_pressed_time(time) +``` **HORIZONTAL AND VERTICAL LISTS** @@ -452,139 +478,159 @@ You can do this either by adding a flag in a dynamic_list() or static_list() cal Perform input and state handling for a scrollbar (a handle that can be dragged/scrolled along a bar) **PARAMETERS** -* ```handle_id``` (string|hash) - Id of the node containing the handle that can be dragged to scroll. -* ```bounds_id``` (string|hash) - Id of the node containing the actual bar that the handle should be dragged along. The size of this area will decide the vertical bounds of the handle. -* ```action_id``` (hash) - Action id as received from on_input() -* ```action``` (table) - Action as received from on_input() -* ```fn``` (function) - Function to call when the scrollbar has been scrolled. The function will get the same state table as described below passed as its first argument -* ```refresh_fn``` (function) - Optional function to call when the state of the list has been updated. Use this to update the visual representation. +* `handle_id` (string|hash) - Id of the node containing the handle that can be dragged to scroll. +* `bounds_id` (string|hash) - Id of the node containing the actual bar that the handle should be dragged along. The size of this area will decide the vertical bounds of the handle. +* `action_id` (hash) - Action id as received from on_input() +* `action` (table) - Action as received from on_input() +* `fn` (function) - Function to call when the scrollbar has been scrolled. The function will get the same state table as described below passed as its first argument +* `refresh_fn` (function) - Optional function to call when the state of the list has been updated. Use this to update the visual representation. **RETURN** -* ```scrollbar``` (table) - State data for the scrollbar +* `scrollbar` (table) - State data for the scrollbar -The ```scrollbar``` table contains the following fields: +The `scrollbar` table contains the following fields: -* ```enabled``` (boolean) - true if the node is enabled -* ```pressed``` (boolean) - true if the handle is pressed (ie mouse/touch down but not yet released) -* ```pressed_now``` (boolean) - true if the handle was pressed this call -* ```released_now``` (boolean) - true if the handle was released this call -* ```over``` (boolean) - true if user action is inside the handle -* ```over_now``` (boolean) - true if user action moved inside the handle this call -* ```out_now``` (boolean) - true if user action moved outside the handle this call -* ```clicked``` (boolean) - true if the input is considered a click (ie pressed and released cycle) -* ```scroll``` (vector3) - Current scroll amount in each direction (0.0 to 1.0) +* `enabled` (boolean) - true if the node is enabled +* `scroll` (vector3) - Current scroll amount in each direction (0.0 to 1.0) **EXAMPLE** - local gooey = require "gooey.gooey" +```lua +local gooey = require "gooey.gooey" - local function on_scrolled(scrollbar) - print("scrolled", scrollbar.scroll.y) - end +local function on_scrolled(scrollbar) + print("scrolled", scrollbar.scroll.y) +end - function on_input(self, action_id, action) - gooey.vertical_scrollbar("handle", "bounds", action_id, action, on_scrolled) - end +function on_input(self, action_id, action) + gooey.vertical_scrollbar("handle", "bounds", action_id, action, on_scrolled) +end +``` **STATE** It is possible to set the scroll amount of a scrollbar. This is useful when updating a scrollbar that belongs to a list when the list was scrolled: - -- scroll 75% of the way - gooey.vertical_scrollbar("handle", "bounds").scroll_to(0, 0.75) +```lua +-- scroll 75% of the way +gooey.vertical_scrollbar("handle", "bounds").scroll_to(0, 0.75) +``` ### gooey.input(node_id, keyboard_type, action_id, action, config, refresh_fn) Perform input and state handling for a text input field **PARAMETERS** -* ```node_id``` (string|hash) - Id of the text node -* ```keyboard_type``` (number) - Keyboard type from gui.KEYBOARD_TYPE_* -* ```action_id``` (hash) - Action id as received from on_input() -* ```action``` (table) - Action as received from on_input() -* ```config``` (table) - Optional configuration values. -* ```refresh_fn``` (function) - Optional function to call when the state of the input field has been updated. Use this to update the visual representation. +* `node_id` (string|hash) - Id of the text node +* `keyboard_type` (number) - Keyboard type from gui.KEYBOARD_TYPE_* +* `action_id` (hash) - Action id as received from on_input() +* `action` (table) - Action as received from on_input() +* `config` (table) - Optional configuration values. +* `refresh_fn` (function) - Optional function to call when the state of the input field has been updated. Use this to update the visual representation. The configuration table accepts the following values: -* ```max_length``` (number) - Maximum length of entered text -* ```empty_text``` (string) - Text to show if the text field is empty -* ```allowed_characters``` (string) - Lua pattern to filter allowed characters (eg "[%a%d]" for alpha numeric) -* ```use_marked_text``` (bool) - Flag to disable the usage of marked (non-committed) text, defaults to true +* `max_length` (number) - Maximum length of entered text +* `empty_text` (string) - Text to show if the text field is empty +* `allowed_characters` (string) - Lua pattern to filter allowed characters (eg "[%a%d]" for alpha numeric) +* `use_marked_text` (bool) - Flag to disable the usage of marked (non-committed) text, defaults to true **RETURN** -* ```input``` (table) - State data for the input field based on current and previous input actions +* `input` (table) - State data for the input field based on current and previous input actions The state table contains the following fields: -* ```node``` (node) - The node itself -* ```node_id``` (node_id) - Hashed id of the node -* ```enabled``` (boolean) - true if the node is enabled -* ```consumed``` (boolean) - true if the input was consumed -* ```over``` (boolean) - true if user action is inside the node -* ```over_now``` (boolean) - true if user action moved inside the node this call -* ```out_now``` (boolean) - true if user action moved outside the node this call -* ```selected``` (boolean) - true if the text field is selected -* ```pressed``` (boolean) - true if the text field is pressed (ie mouse/touch down but not yet released) -* ```pressed_now``` (boolean) - true if the text field was pressed this call -* ```long_pressed``` (boolean) - true if the registered press was a long press or not -* ```released_now``` (boolean) - true if the text field was released this call -* ```selected_now``` (boolean) - true if the text field was selected this call -* ```deselected_now``` (boolean) - true if the text field was deselected this call -* ```text``` (string) - The text in the field -* ```marked_text``` (string) - The marked (non-committed) text -* ```keyboard_type``` (number) -* ```masked_text``` (string) - If the keyboard type is gui.KEYBOARD_TYPE_PASSWORD then this string represents a masked version of the text -* ```masked_marked_text``` (string) - If the keyboard type is gui.KEYBOARD_TYPE_PASSWORD then this string represents a masked version of the marked text -* ```text_width``` (number) - The width of the text -* ```marked_text_width``` (number) - The width of the marked text -* ```total_width``` (number) - The total width of the text including marked text +* `node` (node) - The node itself +* `node_id` (node_id) - Hashed id of the node +* `enabled` (boolean) - true if the node is enabled +* `selected` (boolean) - true if the text field is selected +* `selected_now` (boolean) - true if the text field was selected this call +* `deselected_now` (boolean) - true if the text field was deselected this call +* `text` (string) - The text in the field +* `marked_text` (string) - The marked (non-committed) text +* `keyboard_type` (number) +* `masked_text` (string) - If the keyboard type is gui.KEYBOARD_TYPE_PASSWORD then this string represents a masked version of the text +* `masked_marked_text` (string) - If the keyboard type is gui.KEYBOARD_TYPE_PASSWORD then this string represents a masked version of the marked text +* `text_width` (number) - The width of the text +* `marked_text_width` (number) - The width of the marked text +* `total_width` (number) - The total width of the text including marked text **EXAMPLE** - local gooey = require "gooey.gooey" +```lua +local gooey = require "gooey.gooey" - local function update_input(input) - if input.selected_now then - gui.play_flipbook(input.node, hash("input_selected")) - elseif input.deselected_now then - gui.play_flipbook(input.node, hash("input_normal")) - end +local function update_input(input) + if input.selected_now then + gui.play_flipbook(input.node, hash("input_selected")) + elseif input.deselected_now then + gui.play_flipbook(input.node, hash("input_normal")) end +end - function on_input(self, action_id, action) - gooey.input("input/text", gui.KEYBOARD_TYPE_DEFAULT, action_id, action, nil, update_input) +function on_input(self, action_id, action) + gooey.input("input/text", gui.KEYBOARD_TYPE_DEFAULT, action_id, action, nil, update_input) end +``` **STATE** It is possible to set the state of an input node: - update_input(gooey.input("input/text", gui.KEYBOARD_TYPE_DEFAULT).set_visible(false)) - update_input(gooey.input("input/text", gui.KEYBOARD_TYPE_DEFAULT).set_text("foobar")) +```lua +update_input(gooey.input("input/text", gui.KEYBOARD_TYPE_DEFAULT).set_visible(false)) +update_input(gooey.input("input/text", gui.KEYBOARD_TYPE_DEFAULT).set_text("foobar")) +``` **CONFIG** It is possible to configure the minimum time required to detect a long-press: - gooey.input("input/text").set_long_pressed_time(time) +```lua +gooey.input("input/text").set_long_pressed_time(time) +``` + + +### gooey.group(group_id, action_id, action, group_fn) +Perform input and state handling for a group of components + +**PARAMETERS** +* `group_id` (string|hash) - Id of the group +* `action_id` (hash) - Action id as received from on_input() +* `action` (table) - Action as received from on_input() +* `group_fn` (function) - Components defined within this function belongs to the group + + +**RETURN** +* `group` (table) - State data for the group (focus and consumed input) + +The state table contains the following fields: + +* `focus` (table) - Table with focus info (component and index) +* `consumed` (boolean) - true if the input was consumed by any of the components in the group + + +## Component navigation +Gooey supports component focus and component navigation. Components defined within a group will gain focus when clicked. It is possible to change focused component by sending a `next` or `previous` input action to a group. Components within a group are navigated in the order they are defined. ## Consuming input -Each Gooey component has a ```consumed``` variable in its state table. Consuming input in a gui_script to prevent input propagation can be done by checking the consumed state of each component. If any component has ```consumed``` set to true it is safe to return true from the ```on_input()``` function to prevent input propagation. It is also possible to wrap all component interaction in an input group and check the consumed state for the entire group: - - local gooey = require "gooey.gooey" - - function on_input(self, action_id, action) - local group = gooey.group("group1", function() - gooey.button("button1/bg", action_id, action, on_pressed, function(button) end) - gooey.button("button2/bg", action_id, action, on_pressed, function(button) end) - gooey.radio("radio1/bg", "MYGROUP", action_id, action, function(radio) end, update_radio) - ... - -- and so on - end) - return group.consumed - end +Each Gooey component has a `consumed` variable in its state table. Consuming input in a gui_script to prevent input propagation can be done by checking the consumed state of each component. If any component has `consumed` set to true it is safe to return true from the `on_input()` function to prevent input propagation. It is also possible to wrap all component interaction in an input group and check the consumed state for the entire group: + +```lua +local gooey = require "gooey.gooey" + +function on_input(self, action_id, action) + local group = gooey.group("group1", action_id, action, function() + gooey.button("button1/bg", action_id, action, on_pressed, function(button) end) + gooey.button("button2/bg", action_id, action, on_pressed, function(button) end) + gooey.radio("radio1/bg", "MYGROUP", action_id, action, function(radio) end, update_radio) + ... + -- and so on + end) + return group.consumed +end +``` ## Example app diff --git a/example/components.gui b/example/components.gui index 80358d5..f14802a 100644 --- a/example/components.gui +++ b/example/components.gui @@ -62,6 +62,10 @@ nodes { alpha: 1.0 template_node_child: false size_mode: SIZE_MODE_MANUAL + custom_type: 0 + enabled: true + visible: true + material: "" } nodes { position { @@ -117,6 +121,10 @@ nodes { alpha: 1.0 template_node_child: false size_mode: SIZE_MODE_MANUAL + custom_type: 0 + enabled: true + visible: true + material: "" } nodes { position { @@ -180,11 +188,15 @@ nodes { template_node_child: false text_leading: 1.0 text_tracking: 0.0 + custom_type: 0 + enabled: true + visible: true + material: "" } nodes { position { x: 111.0 - y: 252.0 + y: -35.0 z: 0.0 w: 1.0 } @@ -201,21 +213,21 @@ nodes { w: 1.0 } size { - x: 200.0 - y: 40.0 + x: 220.0 + y: 220.0 z: 0.0 w: 1.0 } color { - x: 1.0 - y: 1.0 - z: 1.0 + x: 0.9490196 + y: 0.9490196 + z: 0.9490196 w: 1.0 } type: TYPE_BOX blend_mode: BLEND_MODE_ALPHA texture: "" - id: "input_text_bg" + id: "dynamiclist" xanchor: XANCHOR_NONE yanchor: YANCHOR_NONE pivot: PIVOT_CENTER @@ -235,6 +247,10 @@ nodes { alpha: 1.0 template_node_child: false size_mode: SIZE_MODE_MANUAL + custom_type: 0 + enabled: true + visible: true + material: "" } nodes { position { @@ -257,52 +273,48 @@ nodes { } size { x: 200.0 - y: 40.0 + y: 200.0 z: 0.0 w: 1.0 } color { - x: 0.0 - y: 0.0 - z: 0.0 + x: 1.0 + y: 1.0 + z: 1.0 w: 1.0 } - type: TYPE_TEXT + type: TYPE_BOX blend_mode: BLEND_MODE_ALPHA - text: "INPUT" - font: "example" - id: "input_text" + texture: "" + id: "dynamiclist_bounds" xanchor: XANCHOR_NONE yanchor: YANCHOR_NONE pivot: PIVOT_CENTER - outline { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } - shadow { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } adjust_mode: ADJUST_MODE_FIT - line_break: false - parent: "input_text_bg" + parent: "dynamiclist" layer: "" inherit_alpha: true + slice9 { + x: 0.0 + y: 0.0 + z: 0.0 + w: 0.0 + } + clipping_mode: CLIPPING_MODE_STENCIL + clipping_visible: true + clipping_inverted: false alpha: 1.0 - outline_alpha: 1.0 - shadow_alpha: 1.0 template_node_child: false - text_leading: 1.0 - text_tracking: 0.0 + size_mode: SIZE_MODE_MANUAL + custom_type: 0 + enabled: true + visible: true + material: "" } nodes { position { - x: 111.0 - y: 196.0 + x: 0.0 + y: 80.0 z: 0.0 w: 1.0 } @@ -333,12 +345,12 @@ nodes { type: TYPE_BOX blend_mode: BLEND_MODE_ALPHA texture: "" - id: "input_alphanumeric_bg" + id: "listitem" xanchor: XANCHOR_NONE yanchor: YANCHOR_NONE pivot: PIVOT_CENTER adjust_mode: ADJUST_MODE_FIT - parent: "buttons" + parent: "dynamiclist_bounds" layer: "" inherit_alpha: true slice9 { @@ -353,6 +365,10 @@ nodes { alpha: 1.0 template_node_child: false size_mode: SIZE_MODE_MANUAL + custom_type: 0 + enabled: true + visible: true + material: "" } nodes { position { @@ -387,9 +403,9 @@ nodes { } type: TYPE_TEXT blend_mode: BLEND_MODE_ALPHA - text: "ALPHA NUMERIC INPUT" + text: "ITEM" font: "example" - id: "input_alphanumeric" + id: "listitem_text" xanchor: XANCHOR_NONE yanchor: YANCHOR_NONE pivot: PIVOT_CENTER @@ -407,7 +423,7 @@ nodes { } adjust_mode: ADJUST_MODE_FIT line_break: false - parent: "input_alphanumeric_bg" + parent: "listitem" layer: "" inherit_alpha: true alpha: 1.0 @@ -416,11 +432,15 @@ nodes { template_node_child: false text_leading: 1.0 text_tracking: 0.0 + custom_type: 0 + enabled: true + visible: true + material: "" } nodes { position { - x: 111.0 - y: -35.0 + x: 400.0 + y: 210.0 z: 0.0 w: 1.0 } @@ -437,27 +457,27 @@ nodes { w: 1.0 } size { - x: 220.0 - y: 220.0 + x: 40.0 + y: 40.0 z: 0.0 w: 1.0 } color { - x: 0.9490196 - y: 0.9490196 - z: 0.9490196 + x: 1.0 + y: 1.0 + z: 1.0 w: 1.0 } type: TYPE_BOX blend_mode: BLEND_MODE_ALPHA texture: "" - id: "dynamiclist" + id: "radio1" xanchor: XANCHOR_NONE yanchor: YANCHOR_NONE pivot: PIVOT_CENTER adjust_mode: ADJUST_MODE_FIT parent: "buttons" - layer: "" + layer: "below" inherit_alpha: true slice9 { x: 0.0 @@ -471,11 +491,15 @@ nodes { alpha: 1.0 template_node_child: false size_mode: SIZE_MODE_MANUAL + custom_type: 0 + enabled: true + visible: true + material: "" } nodes { position { - x: 0.0 - y: 0.0 + x: 400.0 + y: 160.0 z: 0.0 w: 1.0 } @@ -492,8 +516,8 @@ nodes { w: 1.0 } size { - x: 200.0 - y: 200.0 + x: 40.0 + y: 40.0 z: 0.0 w: 1.0 } @@ -506,13 +530,13 @@ nodes { type: TYPE_BOX blend_mode: BLEND_MODE_ALPHA texture: "" - id: "dynamiclist_bounds" + id: "radio2" xanchor: XANCHOR_NONE yanchor: YANCHOR_NONE pivot: PIVOT_CENTER adjust_mode: ADJUST_MODE_FIT - parent: "dynamiclist" - layer: "" + parent: "buttons" + layer: "below" inherit_alpha: true slice9 { x: 0.0 @@ -520,17 +544,21 @@ nodes { z: 0.0 w: 0.0 } - clipping_mode: CLIPPING_MODE_STENCIL + clipping_mode: CLIPPING_MODE_NONE clipping_visible: true clipping_inverted: false alpha: 1.0 template_node_child: false size_mode: SIZE_MODE_MANUAL + custom_type: 0 + enabled: true + visible: true + material: "" } nodes { position { - x: 0.0 - y: 80.0 + x: 400.0 + y: 110.0 z: 0.0 w: 1.0 } @@ -547,7 +575,7 @@ nodes { w: 1.0 } size { - x: 200.0 + x: 40.0 y: 40.0 z: 0.0 w: 1.0 @@ -561,13 +589,13 @@ nodes { type: TYPE_BOX blend_mode: BLEND_MODE_ALPHA texture: "" - id: "listitem" + id: "radio3" xanchor: XANCHOR_NONE yanchor: YANCHOR_NONE pivot: PIVOT_CENTER adjust_mode: ADJUST_MODE_FIT - parent: "dynamiclist_bounds" - layer: "" + parent: "buttons" + layer: "below" inherit_alpha: true slice9 { x: 0.0 @@ -581,11 +609,15 @@ nodes { alpha: 1.0 template_node_child: false size_mode: SIZE_MODE_MANUAL + custom_type: 0 + enabled: true + visible: true + material: "" } nodes { position { - x: 0.0 - y: 0.0 + x: 400.0 + y: 311.0 z: 0.0 w: 1.0 } @@ -602,53 +634,106 @@ nodes { w: 1.0 } size { - x: 200.0 + x: 40.0 y: 40.0 z: 0.0 w: 1.0 } color { - x: 0.0 - y: 0.0 - z: 0.0 + x: 1.0 + y: 1.0 + z: 1.0 w: 1.0 } - type: TYPE_TEXT + type: TYPE_BOX blend_mode: BLEND_MODE_ALPHA - text: "ITEM" - font: "example" - id: "listitem_text" + texture: "" + id: "checkbox" xanchor: XANCHOR_NONE yanchor: YANCHOR_NONE pivot: PIVOT_CENTER - outline { + adjust_mode: ADJUST_MODE_FIT + parent: "buttons" + layer: "below" + inherit_alpha: true + slice9 { + x: 0.0 + y: 0.0 + z: 0.0 + w: 0.0 + } + clipping_mode: CLIPPING_MODE_NONE + clipping_visible: true + clipping_inverted: false + alpha: 1.0 + template_node_child: false + size_mode: SIZE_MODE_MANUAL + custom_type: 0 + enabled: true + visible: true + material: "" +} +nodes { + position { + x: -276.0 + y: -11.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { x: 1.0 y: 1.0 z: 1.0 w: 1.0 } - shadow { + size { + x: 5.0 + y: 5.0 + z: 1.0 + w: 1.0 + } + color { x: 1.0 y: 1.0 z: 1.0 w: 1.0 } + type: TYPE_PIE + blend_mode: BLEND_MODE_ALPHA + texture: "" + id: "focus" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER adjust_mode: ADJUST_MODE_FIT - line_break: false - parent: "listitem" + parent: "buttons" layer: "" inherit_alpha: true + outerBounds: PIEBOUNDS_ELLIPSE + innerRadius: 0.0 + perimeterVertices: 10 + pieFillAngle: 360.0 + clipping_mode: CLIPPING_MODE_NONE + clipping_visible: true + clipping_inverted: false alpha: 1.0 - outline_alpha: 1.0 - shadow_alpha: 1.0 template_node_child: false - text_leading: 1.0 - text_tracking: 0.0 + size_mode: SIZE_MODE_MANUAL + custom_type: 0 + enabled: true + visible: true + material: "" } nodes { position { - x: 400.0 - y: 210.0 + x: 111.0 + y: 252.0 z: 0.0 w: 1.0 } @@ -665,45 +750,57 @@ nodes { w: 1.0 } size { - x: 40.0 + x: 200.0 y: 40.0 z: 0.0 w: 1.0 } color { - x: 1.0 - y: 1.0 - z: 1.0 + x: 0.0 + y: 0.0 + z: 0.0 w: 1.0 } - type: TYPE_BOX + type: TYPE_TEXT blend_mode: BLEND_MODE_ALPHA - texture: "" - id: "radio1" + text: "INPUT" + font: "example" + id: "input_text" xanchor: XANCHOR_NONE yanchor: YANCHOR_NONE pivot: PIVOT_CENTER + outline { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + shadow { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } adjust_mode: ADJUST_MODE_FIT + line_break: false parent: "buttons" - layer: "" + layer: "text" inherit_alpha: true - slice9 { - x: 0.0 - y: 0.0 - z: 0.0 - w: 0.0 - } - clipping_mode: CLIPPING_MODE_NONE - clipping_visible: true - clipping_inverted: false alpha: 1.0 + outline_alpha: 1.0 + shadow_alpha: 1.0 template_node_child: false - size_mode: SIZE_MODE_MANUAL + text_leading: 1.0 + text_tracking: 0.0 + custom_type: 0 + enabled: true + visible: true + material: "" } nodes { position { - x: 400.0 - y: 160.0 + x: 0.0 + y: 0.0 z: 0.0 w: 1.0 } @@ -720,7 +817,7 @@ nodes { w: 1.0 } size { - x: 40.0 + x: 200.0 y: 40.0 z: 0.0 w: 1.0 @@ -734,13 +831,13 @@ nodes { type: TYPE_BOX blend_mode: BLEND_MODE_ALPHA texture: "" - id: "radio2" + id: "input_text_bg" xanchor: XANCHOR_NONE yanchor: YANCHOR_NONE pivot: PIVOT_CENTER adjust_mode: ADJUST_MODE_FIT - parent: "buttons" - layer: "" + parent: "input_text" + layer: "below" inherit_alpha: true slice9 { x: 0.0 @@ -754,11 +851,15 @@ nodes { alpha: 1.0 template_node_child: false size_mode: SIZE_MODE_MANUAL + custom_type: 0 + enabled: true + visible: true + material: "" } nodes { position { - x: 400.0 - y: 110.0 + x: 111.0 + y: 196.0 z: 0.0 w: 1.0 } @@ -775,45 +876,57 @@ nodes { w: 1.0 } size { - x: 40.0 + x: 200.0 y: 40.0 z: 0.0 w: 1.0 } color { - x: 1.0 - y: 1.0 - z: 1.0 + x: 0.0 + y: 0.0 + z: 0.0 w: 1.0 } - type: TYPE_BOX + type: TYPE_TEXT blend_mode: BLEND_MODE_ALPHA - texture: "" - id: "radio3" + text: "ALPHA NUMERIC INPUT" + font: "example" + id: "input_alphanumeric" xanchor: XANCHOR_NONE yanchor: YANCHOR_NONE pivot: PIVOT_CENTER + outline { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + shadow { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } adjust_mode: ADJUST_MODE_FIT + line_break: false parent: "buttons" - layer: "" + layer: "text" inherit_alpha: true - slice9 { - x: 0.0 - y: 0.0 - z: 0.0 - w: 0.0 - } - clipping_mode: CLIPPING_MODE_NONE - clipping_visible: true - clipping_inverted: false alpha: 1.0 + outline_alpha: 1.0 + shadow_alpha: 1.0 template_node_child: false - size_mode: SIZE_MODE_MANUAL + text_leading: 1.0 + text_tracking: 0.0 + custom_type: 0 + enabled: true + visible: true + material: "" } nodes { position { - x: 400.0 - y: 311.0 + x: 0.0 + y: 0.0 z: 0.0 w: 1.0 } @@ -830,7 +943,7 @@ nodes { w: 1.0 } size { - x: 40.0 + x: 200.0 y: 40.0 z: 0.0 w: 1.0 @@ -844,13 +957,13 @@ nodes { type: TYPE_BOX blend_mode: BLEND_MODE_ALPHA texture: "" - id: "checkbox" + id: "input_alphanumeric_bg" xanchor: XANCHOR_NONE yanchor: YANCHOR_NONE pivot: PIVOT_CENTER adjust_mode: ADJUST_MODE_FIT - parent: "buttons" - layer: "" + parent: "input_alphanumeric" + layer: "below" inherit_alpha: true slice9 { x: 0.0 @@ -864,11 +977,15 @@ nodes { alpha: 1.0 template_node_child: false size_mode: SIZE_MODE_MANUAL + custom_type: 0 + enabled: true + visible: true + material: "" } nodes { position { - x: 40.0 - y: 1094.0 + x: -63.0 + y: 526.0 z: 0.0 w: 1.0 } @@ -904,6 +1021,7 @@ nodes { yanchor: YANCHOR_NONE pivot: PIVOT_CENTER adjust_mode: ADJUST_MODE_FIT + parent: "buttons" layer: "" inherit_alpha: true slice9 { @@ -918,6 +1036,10 @@ nodes { alpha: 1.0 template_node_child: false size_mode: SIZE_MODE_MANUAL + custom_type: 0 + enabled: true + visible: true + material: "" } nodes { position { @@ -981,6 +1103,10 @@ nodes { template_node_child: false text_leading: 1.0 text_tracking: 0.0 + custom_type: 0 + enabled: true + visible: true + material: "" } layers { name: "below" diff --git a/example/components.gui_script b/example/components.gui_script index 122ce0a..f76b240 100644 --- a/example/components.gui_script +++ b/example/components.gui_script @@ -28,12 +28,12 @@ local function update_checkbox(checkbox) end local function update_button(button) - if button.over_now then + if button.pressed_now then + utils.shake(button.node, vmath.vector3(1)) + elseif button.over_now then gui.set_color(button.node, COLOR_LIGHTGREY) elseif button.out_now then gui.set_color(button.node, COLOR_WHITE) - elseif button.pressed_now then - utils.shake(button.node, vmath.vector3(1)) end end @@ -64,7 +64,11 @@ function init(self) end function on_input(self, action_id, action) - local group = gooey.group("group1", function() + local group = gooey.group("components", action_id, action, function() + gooey.button("back", action_id, action, function(button) + msg.post("controller:/go", "show_menu") + end, update_button) + gooey.button("button", action_id, action, function(button) if button.long_pressed then print("Button was long pressed") @@ -73,9 +77,12 @@ function on_input(self, action_id, action) end end, update_button) - gooey.button("back", action_id, action, function(button) - msg.post("controller:/go", "show_menu") - end, update_button) + gooey.input("input_text", gui.KEYBOARD_TYPE_DEFAULT, action_id, action, { empty_text = "EMPTY, MAX 8 CHARS", max_length = 8 }, update_input) + gooey.input("input_alphanumeric", gui.KEYBOARD_TYPE_DEFAULT, action_id, action, { empty_text = "ALPHA NUMERIC CHARS", allowed_characters = "[%a%d%s]", use_marked_text = false}, update_input) + + gooey.dynamic_list("dynamiclist", "dynamiclist_bounds", "listitem", self.list_data, action_id, action, nil, function(list) + print("selected item", list.selected_item.index, list.data[list.selected_item.index]) + end, update_list) gooey.checkbox("checkbox", action_id, action, function(checkbox) print("checkbox", checkbox.checked) @@ -92,13 +99,15 @@ function on_input(self, action_id, action) print("radio 3", radio.selected) end, update_radiobutton) end) - - gooey.input("input_text", gui.KEYBOARD_TYPE_DEFAULT, action_id, action, { empty_text = "EMPTY, MAX 8 CHARS", max_length = 8 }, update_input) - gooey.input("input_alphanumeric", gui.KEYBOARD_TYPE_DEFAULT, action_id, action, { empty_text = "ALPHA NUMERIC CHARS", allowed_characters = "[%a%d%s]", use_marked_text = false}, update_input) - - gooey.dynamic_list("dynamiclist", "dynamiclist_bounds", "listitem", self.list_data, action_id, action, nil, function(list) - print("selected item", list.selected_item.index, list.data[list.selected_item.index]) - end, update_list) end) + + if group.focus.component then + local focus_indicator = gui.get_node("focus") + local focus_node = gui.get_node(group.focus.component.id) + local pos = gui.get_position(focus_node) + local size = gui.get_size(focus_node) + pos.x = pos.x - size.x / 2 - 10 + gui.set_position(focus_indicator, pos) + end return group.consumed end diff --git a/example/menu.gui_script b/example/menu.gui_script index 83a78ac..8d9fadb 100644 --- a/example/menu.gui_script +++ b/example/menu.gui_script @@ -5,7 +5,7 @@ function init(self) end function on_input(self, action_id, action) - local group = gooey.group("group1", function() + local group = gooey.group("group1", action_id, action, function() gooey.button("components", action_id, action, function() msg.post("controller:/go", "show_components") end) diff --git a/gooey/actions.lua b/gooey/actions.lua index ec3f9dc..6e8ef32 100644 --- a/gooey/actions.lua +++ b/gooey/actions.lua @@ -7,5 +7,8 @@ M.SCROLL_DOWN = hash("scroll_down") M.TEXT = hash("text") M.MARKED_TEXT = hash("marked_text") M.BACKSPACE = hash("backspace") +M.NEXT = hash("next") +M.PREVIOUS = hash("previous") +M.SELECT = hash("select") return M \ No newline at end of file diff --git a/gooey/gooey.lua b/gooey/gooey.lua index 66e1b5a..145081e 100644 --- a/gooey/gooey.lua +++ b/gooey/gooey.lua @@ -5,6 +5,7 @@ local radio = require "gooey.internal.radio" local list = require "gooey.internal.list" local input = require "gooey.internal.input" local scrollbar = require "gooey.internal.scrollbar" +local actions = require "gooey.actions" local M = {} @@ -35,7 +36,7 @@ end function M.create_theme() local theme = {} - + theme.is_enabled = function(component) if component.node then return M.is_enabled(component.node) @@ -187,11 +188,27 @@ end -- @param id -- @param fn Interact with gooey components inside this function -- @return Group state -function M.group(id, fn) +function M.group(id, action_id, action, fn) assert(id, "You must provide a group id") assert(fn, "You must provide a group function") - groups[id] = groups[id] or { consumed = false, components = {} } + if not groups[id] then + groups[id] = { + consumed = false, -- true if a component in the group consumed input + components = {}, -- list of all components in the group + focus = { + component = nil, -- component with focus + index = nil, -- index of component with focus + }, + } + end local group = groups[id] + local components = group.components + local focus = group.focus + + -- clear list of components + for i=1,#components do + components[i] = nil + end -- set current group and call the group function -- then reset current group again once we're done @@ -199,16 +216,95 @@ function M.group(id, fn) fn() current_group = nil + -- exit early if there are no components in the group + if #components == 0 then + focus.component = nil + focus.index = nil + return group + end + -- go through the components in the group and check if -- any of them consumed input - local components = group.components + -- also check which component has focus and if another + -- component was selected and should gain focus local consumed = false + local current_focus_index = nil + local new_focus_index = nil for i=1,#components do - consumed = components[i].consumed or consumed - components[i] = nil + local component = components[i] + consumed = component.consumed or consumed + if component.focus then + current_focus_index = i + elseif component.released_now or component.released_item_now then + new_focus_index = i + end + end + + if not new_focus_index then + new_focus_index = current_focus_index + end + + -- assign focus to the next component or first if + -- no component currently has focus + if not consumed then + if action_id == actions.NEXT then + if action.pressed then + if new_focus_index then + new_focus_index = new_focus_index + 1 + if new_focus_index > #components then + new_focus_index = 1 + end + else + new_focus_index = 1 + end + end + consumed = true + elseif action_id == actions.PREVIOUS then + if action.pressed then + if new_focus_index then + new_focus_index = new_focus_index - 1 + if new_focus_index == 0 then + new_focus_index = #components + end + else + new_focus_index = 1 + end + end + consumed = true + end + end + + -- changing focus + if current_focus_index ~= new_focus_index then + if current_focus_index then + local component = components[current_focus_index] + component.focus = false + component.refresh() + focus.index = nil + focus.component = nil + end + if new_focus_index then + local component = components[new_focus_index] + component.focus = true + component.refresh() + focus.index = new_focus_index + focus.component = component + end end group.consumed = consumed return group end +function M.set_focus(group, index) + assert(group) + assert(index) + local component = group.components[index] + if component then + component.focus = true + component.refresh() + group.focus.index = index + group.focus.component = component + end +end + return M \ No newline at end of file diff --git a/gooey/internal/core.lua b/gooey/internal/core.lua index de90d2d..a3209fe 100644 --- a/gooey/internal/core.lua +++ b/gooey/internal/core.lua @@ -11,11 +11,14 @@ local function handle_action(component, action_id, action) component.long_pressed_time = component.long_pressed_time or 1.5 if not component.touch_id or component.touch_id == action.id then local over = gui.pick_node(component.node, action.x, action.y) + if component.focus and action_id == actions.SELECT then + over = true + end component.over_now = over and not component.over component.out_now = not over and component.over component.over = over - local touch = action_id == actions.TOUCH or action_id == actions.MULTITOUCH + local touch = action_id == actions.TOUCH or action_id == actions.MULTITOUCH or action_id == actions.SELECT local pressed = touch and action.pressed and component.over local released = touch and action.released if pressed then @@ -111,6 +114,7 @@ end -- @return instance Instance data for the node (public data) -- @return state Internal state of the node (private data) function M.instance(id, instances, functions) + id = M.to_hash(id) local key = M.to_key(id) local instance = instances[key] -- detect a reload (unload and load cycle) and start with an @@ -128,6 +132,7 @@ function M.instance(id, instances, functions) instances[key] = instances[key] or { __script = script_instance } if not instances[key].data then local data = {} + data.id = id instances[key].data = data for name,fn in pairs(functions or {}) do data[name] = function(...) diff --git a/gooey/internal/input.lua b/gooey/internal/input.lua index f373ee2..e554b2a 100644 --- a/gooey/internal/input.lua +++ b/gooey/internal/input.lua @@ -147,7 +147,10 @@ function M.input(node_id, keyboard_type, action_id, action, config, refresh_fn) input.marked_text = "" gui.reset_keyboard() gui.show_keyboard(keyboard_type, true) - elseif input.selected and action.pressed and action_id == actions.TOUCH and not input.over then + elseif input.selected + and action.pressed + and (action_id == actions.TOUCH or action_id == actions.SELECT or action_id == actions.NEXT) + and not input.over then input.selected = false input.deselected_now = true input.text = input.text .. (not use_marked_text and input.marked_text or "") diff --git a/gooey/internal/list.lua b/gooey/internal/list.lua index f4027c9..2eafe54 100644 --- a/gooey/internal/list.lua +++ b/gooey/internal/list.lua @@ -108,9 +108,8 @@ end -- get a list instance and set up some basics of a list on the instance local function get_instance(list_id, stencil_id, refresh_fn, lists) - stencil_id = core.to_hash(stencil_id) - local list = core.instance(stencil_id, lists, LIST) - list.id = list_id + list_id = core.to_hash(list_id) + local list = core.instance(list_id, lists, LIST) list.scroll = list.scroll or vmath.vector3() list.stencil = list.stencil or gui.get_node(stencil_id) list.stencil_size = list.stencil_size or gui.get_size(list.stencil) diff --git a/input/game.input_binding b/input/game.input_binding index cf250a7..6f692bd 100644 --- a/input/game.input_binding +++ b/input/game.input_binding @@ -2,6 +2,22 @@ key_trigger { input: KEY_BACKSPACE action: "backspace" } +key_trigger { + input: KEY_TAB + action: "next" +} +key_trigger { + input: KEY_SPACE + action: "select" +} +key_trigger { + input: KEY_UP + action: "previous" +} +key_trigger { + input: KEY_DOWN + action: "next" +} mouse_trigger { input: MOUSE_BUTTON_1 action: "touch"