Merge pull request #292 from Insality/shaders

All changes to Develop
This commit is contained in:
Maksim Tuprikov
2025-02-09 14:43:19 +02:00
committed by GitHub
239 changed files with 10136 additions and 21428 deletions

View File

@@ -1 +1,2 @@
/dist /dist
/.deployer_cache

2
.gitignore vendored
View File

@@ -15,3 +15,5 @@ deployer_version_settings.txt
bob*.jar bob*.jar
manifest.private.der manifest.private.der
manifest.public.der manifest.public.der
/.editor_settings
/.deployer_cache

View File

@@ -29,7 +29,7 @@
"utils/annotations_manual.lua" "utils/annotations_manual.lua"
], ],
"Lua.runtime.pathStrict": true, "Lua.runtime.pathStrict": true,
"Lua.diagnostics.libraryFiles": "Enable", "Lua.diagnostics.libraryFiles": "Disable",
"Lua.runtime.version": "Lua 5.1", "Lua.runtime.version": "Lua 5.1",
"Lua.workspace.library": [ "Lua.workspace.library": [
"~/Library/Application Support/Code/User/globalStorage/astronachos.defold", "~/Library/Application Support/Code/User/globalStorage/astronachos.defold",

107
README.md
View File

@@ -17,13 +17,15 @@ In this example you can inspect a variety of **Druid** components and see how th
## Setup ## Setup
### Dependency ### [Dependency](https://www.defold.com/manuals/libraries/)
To integrate the **Druid** extension into your own project, add this project as a [dependency](https://www.defold.com/manuals/libraries/) in your **Defold** game. Open your `game.project` file and add the following line to the dependencies field under the project section: Open your `game.project` file and add the following line to the dependencies field under the project section:
**Druid v1.0** **[Druid](https://github.com/Insality/druid/archive/refs/tags/1.0.zip)**
> [https://github.com/Insality/druid/archive/refs/tags/1.0.zip](https://github.com/Insality/druid/archive/refs/tags/1.0.zip) ```
https://github.com/Insality/druid/archive/refs/tags/1.0.zip
```
Here is a list of [all releases](https://github.com/Insality/druid/releases). Here is a list of [all releases](https://github.com/Insality/druid/releases).
@@ -87,12 +89,60 @@ end
function on_input(self, action_id, action) function on_input(self, action_id, action)
return self.druid:on_input(action_id, action) return self.druid:on_input(action_id, action)
end end
``` ```
For all **Druid** instance functions, [see here](https://insality.github.io/druid/modules/DruidInstance.html). For all **Druid** instance functions, [see here](https://insality.github.io/druid/modules/DruidInstance.html).
### Default GUI Script
Put the following code in your GUI script to start using **Druid**.
```lua
local druid = require("druid.druid")
function init(self)
self.druid = druid.new(self)
end
function final(self)
self.druid:final()
end
function update(self, dt)
self.druid:update(dt)
end
function on_message(self, message_id, message, sender)
self.druid:on_message(message_id, message, sender)
end
function on_input(self, action_id, action)
return self.druid:on_input(action_id, action)
end
```
### Default Widget Template
Create a new lua file to create a new widget class. This widget can be created with `self.druid:new_widget(widget_class, [template], [nodes])`
```lua
local M = {}
function M:init()
self.root = self:get_node("root")
self.button = self.druid:new_button("button", self.on_click)
end
function M:on_click()
print("Button clicked!")
end
return M
```
### API Documentation ### API Documentation
**Druid** offers a wide range of components and functions. To facilitate usage, **Druid** provides comprehensive API documentation with examples and annotations. **Druid** offers a wide range of components and functions. To facilitate usage, **Druid** provides comprehensive API documentation with examples and annotations.
@@ -119,45 +169,38 @@ Here is full **Druid** components list.
| Name | Description | Example | <div style="width:200px">Preview</div> | | Name | Description | Example | <div style="width:200px">Preview</div> |
|------|-------------|---------|---------| |------|-------------|---------|---------|
| **[Button](https://insality.github.io/druid/modules/Button.html)** | Logic over GUI Node. Handle the user click interactions: click, long click, double click, etc. | [Button Example](https://insality.github.io/druid/druid/?example=general_buttons) | <img src="media/preview/button.gif" width="200" height="100"> | | **[Button](https://insality.github.io/druid/modules/Button.html)** | Logic over GUI Node. Handle the user click interactions: click, long click, double click, etc. | [Button Example](https://insality.github.io/druid/druid/?example=ui_example_basic_button) | <img src="media/preview/button.gif" width="200" height="100"> |
| **[Text](https://insality.github.io/druid/modules/Text.html)** | Logic over GUI Text. By default Text component fit the text inside text node size area with different adjust modes. | [Text Example](https://insality.github.io/druid/druid/?example=texts_general) | <img src="media/preview/text.gif" width="200" height="100"> | | **[Text](https://insality.github.io/druid/modules/Text.html)** | Logic over GUI Text. By default Text component fit the text inside text node size area with different adjust modes. | [Text Example](https://insality.github.io/druid/druid/?example=ui_example_basic_text) | <img src="media/preview/text.gif" width="200" height="100"> |
| **[Scroll](https://insality.github.io/druid/modules/Scroll.html)** | Logic over two GUI Nodes: input and content. Provides basic behaviour for scrollable content. | [Scroll Example](https://insality.github.io/druid/druid/?example=general_scroll) | <img src="media/preview/scroll.gif" width="200" height="100"> | | **[Scroll](https://insality.github.io/druid/modules/Scroll.html)** | Logic over two GUI Nodes: input and content. Provides basic behaviour for scrollable content. | [Scroll Example](https://insality.github.io/druid/druid/?example=ui_example_basic_scroll) | <img src="media/preview/scroll.gif" width="200" height="100"> |
| **[Blocker](https://insality.github.io/druid/modules/Blocker.html)** | Logic over GUI Node. Don't pass any user input below node area size. | [Blocker Example](https://insality.github.io/druid/druid/?example=timer) | <img src="media/preview/blocker.gif" width="200" height="100"> | | **[Blocker](https://insality.github.io/druid/modules/Blocker.html)** | Logic over GUI Node. Don't pass any user input below node area size. | [Blocker Example](https://insality.github.io/druid/druid/?example=ui_example_basic_blocker) | <img src="media/preview/blocker.gif" width="200" height="100"> |
| **[Back Handler](https://insality.github.io/druid/modules/BackHandler.html)** | Call callback on user "Back" action. It's a Android back button or keyboard backspace key | [Back Handler Example](https://insality.github.io/druid/druid/?example=timer) | <img src="media/preview/back_handler.gif" width="200" height="100"> | | **[Back Handler](https://insality.github.io/druid/modules/BackHandler.html)** | Call callback on user "Back" action. It's a Android back button or keyboard backspace key | [Back Handler Example](https://insality.github.io/druid/druid/?example=ui_example_basic_back_handler) | <img src="media/preview/back_handler.gif" width="200" height="100"> |
| **[Static Grid](https://insality.github.io/druid/modules/StaticGrid.html)** | Logic over GUI Node. Component to manage node positions with all equal node sizes. | [Static Gid Example](https://insality.github.io/druid/druid/?example=general_grid) | <img src="media/preview/static_grid.gif" width="200" height="100"> | | **[Static Grid](https://insality.github.io/druid/modules/StaticGrid.html)** | Logic over GUI Node. Component to manage node positions with all equal node sizes. | [Static Gid Example](https://insality.github.io/druid/druid/?example=ui_example_basic_grid) | <img src="media/preview/static_grid.gif" width="200" height="100"> |
| **[Hover](https://insality.github.io/druid/modules/Hover.html)** | Logic over GUI Node. Handle hover action over node. For both: mobile touch and mouse cursor. | [Hover Example](https://insality.github.io/druid/druid/?example=timer) | <img src="media/preview/hover.gif" width="200" height="100"> | | **[Hover](https://insality.github.io/druid/modules/Hover.html)** | Logic over GUI Node. Handle hover action over node. For both: mobile touch and mouse cursor. | [Hover Example](https://insality.github.io/druid/druid/?example=ui_example_basic_hover) | <img src="media/preview/hover.gif" width="200" height="100"> |
| **[Swipe](https://insality.github.io/druid/modules/Swipe.html)** | Logic over GUI Node. Handle swipe gestures over node. | [Swipe Example](https://insality.github.io/druid/druid/?example=general_swipe) | <img src="media/preview/swipe.gif" width="200" height="100"> | | **[Swipe](https://insality.github.io/druid/modules/Swipe.html)** | Logic over GUI Node. Handle swipe gestures over node. | [Swipe Example](https://insality.github.io/druid/druid/?example=ui_example_basic_swipe) | <img src="media/preview/swipe.gif" width="200" height="100"> |
| **[Drag](https://insality.github.io/druid/modules/Drag.html)** | Logic over GUI Node. Handle drag input actions. Can be useful to make on screen controlls. | [Drag Example](https://insality.github.io/druid/druid/?example=general_drag) | <img src="media/preview/drag.gif" width="200" height="100"> | | **[Drag](https://insality.github.io/druid/modules/Drag.html)** | Logic over GUI Node. Handle drag input actions. Can be useful to make on screen controlls. | [Drag Example](https://insality.github.io/druid/druid/?example=ui_example_basic_drag) | <img src="media/preview/drag.gif" width="200" height="100"> |
### Extended components ### Extended components
> Extended components before usage should be registered in **Druid** with [`druid.register()`](https://insality.github.io/druid/modules/Druid.html#druid.register) function.
> On usage of unregistered **Druid** component the next log will be shown in the console.
```
local data_list = require("druid.extended.data_list")
druid.register("data_list", data_list)
```
| Name | Description | Example | <div style="width:200px">Preview</div> | | Name | Description | Example | <div style="width:200px">Preview</div> |
|------|-------------|---------|---------| |------|-------------|---------|---------|
| **[Data List](https://insality.github.io/druid/modules/DataList.html)** | Logic over Scroll and Grid components. Create only visible GUI nodes or components to make "infinity" scroll befaviour | [Data List Example](https://insality.github.io/druid/druid/?example=general_data_list) | <img src="media/preview/data_list.gif" width="200" height="100"> | | **[Data List](https://insality.github.io/druid/modules/DataList.html)** | Logic over Scroll and Grid components. Create only visible GUI nodes or components to make "infinity" scroll befaviour | [Data List Example](https://insality.github.io/druid/druid/?example=ui_example_data_list_basic) | <img src="media/preview/data_list.gif" width="200" height="100"> |
| **[Input](https://insality.github.io/druid/modules/Input.html)** | Logic over GUI Node and GUI Text (or Text component). Provides basic user text input. | [Input Example](https://insality.github.io/druid/druid/?example=general_input) | <img src="media/preview/input.gif" width="200" height="100"> | | **[Input](https://insality.github.io/druid/modules/Input.html)** | Logic over GUI Node and GUI Text (or Text component). Provides basic user text input. | [Input Example](https://insality.github.io/druid/druid/?example=ui_example_basic_input) | <img src="media/preview/input.gif" width="200" height="100"> |
| **[Lang text](https://insality.github.io/druid/modules/LangText.html)** | Logic over Text component to handle localization. Can be translated in real-time with `druid.on_language_change` | [Lang Text Example](https://insality.github.io/druid/druid/?example=timer) | <img src="media/preview/lang_text.gif" width="200" height="100"> | | **[Lang text](https://insality.github.io/druid/modules/LangText.html)** | Logic over Text component to handle localization. Can be translated in real-time with `druid.on_language_change` | [Lang Text Example](https://insality.github.io/druid/druid/?example=ui_example_window_language) | <img src="media/preview/lang_text.gif" width="200" height="100"> |
| **[Progress](https://insality.github.io/druid/modules/Progress.html)** | Logic over GUI Node. Handle node size and scale to handle progress node size. | [Progress Example](https://insality.github.io/druid/druid/?example=general_progress_bar) | <img src="media/preview/progress.gif" width="200" height="100"> | | **[Progress](https://insality.github.io/druid/modules/Progress.html)** | Logic over GUI Node. Handle node size and scale to handle progress node size. | [Progress Example](https://insality.github.io/druid/druid/?example=ui_example_basic_progress_bar) | <img src="media/preview/progress.gif" width="200" height="100"> |
| **[Slider](https://insality.github.io/druid/modules/Slider.html)** | Logic over GUI Node. Handle draggable node with position restrictions. | [Slider Example](https://insality.github.io/druid/druid/?example=general_sliders) | <img src="media/preview/slider.gif" width="200" height="100"> | | **[Slider](https://insality.github.io/druid/modules/Slider.html)** | Logic over GUI Node. Handle draggable node with position restrictions. | [Slider Example](https://insality.github.io/druid/druid/?example=ui_example_basic_slider) | <img src="media/preview/slider.gif" width="200" height="100"> |
| **[Timer](https://insality.github.io/druid/modules/Timer.html)** | Logic over GUI Text. Handle basic timer functions. | [Timer Example](https://insality.github.io/druid/druid/?example=timer) | <img src="media/preview/timer.gif" width="200" height="100"> | | **[Timer](https://insality.github.io/druid/modules/Timer.html)** | Logic over GUI Text. Handle basic timer functions. | [Timer Example](https://insality.github.io/druid/druid/?example=ui_example_basic_timer) | <img src="media/preview/timer.gif" width="200" height="100"> |
| **[Hotkey](https://insality.github.io/druid/modules/Hotkey.html)** | Allow to set callbacks for keyboard hotkeys with key modificators. | [Hotkey Example](https://insality.github.io/druid/druid/?example=general_hotkey) | <img src="media/preview/hotkey.gif" width="200" height="100"> | | **[Hotkey](https://insality.github.io/druid/modules/Hotkey.html)** | Allow to set callbacks for keyboard hotkeys with key modificators. | [Hotkey Example](https://insality.github.io/druid/druid/?example=ui_example_basic_hotkey) | <img src="media/preview/hotkey.gif" width="200" height="100"> |
| **[Layout](https://insality.github.io/druid/modules/Layout.html)** | Logic over GUI Node. Arrange nodes inside layout node with margin/paddings settings. | [Layout Example](https://insality.github.io/druid/druid/?example=general_layout) | <img src="media/preview/layout.gif" width="200" height="100"> | | **[Layout](https://insality.github.io/druid/modules/Layout.html)** | Logic over GUI Node. Arrange nodes inside layout node with margin/paddings settings. | [Layout Example](https://insality.github.io/druid/druid/?example=ui_example_layout_basic) | <img src="media/preview/layout.gif" width="200" height="100"> |
| **[Rich Input](https://insality.github.io/druid/modules/RichInput.html)** | Logic over GUI Node and GUI Text (or Text component). Provides rich text input with different styles and text formatting. | [Rich Input Example](https://insality.github.io/druid/druid/?example=general_rich_input) | <img src="media/preview/rich_input.gif" width="200" height="100"> | | **[Rich Input](https://insality.github.io/druid/modules/RichInput.html)** | Logic over GUI Node and GUI Text (or Text component). Provides rich text input with different styles and text formatting. | [Rich Input Example](https://insality.github.io/druid/druid/?example=ui_example_basic_rich_input) | <img src="media/preview/rich_input.gif" width="200" height="100"> |
| **[Rich Text](https://insality.github.io/druid/modules/RichText.html)** | Logic over GUI Text. Provides rich text formatting with different styles and text formatting. | [Rich Text Example](https://insality.github.io/druid/druid/?example=general_rich_text) | <img src="media/preview/rich_text.gif" width="200" height="100"> | | **[Rich Text](https://insality.github.io/druid/modules/RichText.html)** | Logic over GUI Text. Provides rich text formatting with different styles and text formatting. | [Rich Text Example](https://insality.github.io/druid/druid/?example=ui_example_basic_rich_text) | <img src="media/preview/rich_text.gif" width="200" height="100"> |
For a complete overview, see: **_[components.md](docs_md/01-components.md)_**. For a complete overview, see: **_[components.md](docs_md/01-components.md)_**.
## Druid Events ## Druid Events
Any **Druid** components as callbacks use [Druid Events](https://insality.github.io/druid/modules/DruidEvent.html). In component API ([button example](https://insality.github.io/druid/modules/Button.html#on_click)) pointed list of component events. You can manually subscribe to these events with the following API: Any **Druid** components as callbacks use [Druid Events](https://insality.github.io/druid/modules/druid.event.html). In component API ([button example](https://insality.github.io/druid/modules/Button.html#on_click)) pointed list of component events. You can manually subscribe to these events with the following API:
- **event:subscribe**(callback) - **event:subscribe**(callback)

View File

@@ -1,18 +0,0 @@
project='Druid'
title='Defold Druid UI Framework'
description='Documentation for Druid Framework'
file={"./druid",
exclude = {
"./druid/styles/",
"./druid/templates/",
"./druid/annotations.lua",
"./druid/custom/rich_text/module",
}
}
package='druid'
sort=true
dir='./docs'
style='!fixed'
topics={}
use_markdown_titles=true
no_space_before_args=true

View File

@@ -216,7 +216,7 @@ Static grid have constant node size, so it possible to calculate node positions
Static grid can shift elements on add/remove functions. Static grid can shift elements on add/remove functions.
### Setup ### Setup
Create component with druid: `grid = druid:new_static_grid(parent_node, prefab_node, max_in_row_elements)` Create component with druid: `grid = druid:new_grid(parent_node, prefab_node, max_in_row_elements)`
### Notes ### Notes
- On _add node_ grid will set nodeup parent to _parent_node_ - On _add node_ grid will set nodeup parent to _parent_node_
@@ -227,33 +227,6 @@ Create component with druid: `grid = druid:new_static_grid(parent_node, prefab_
- _Prefab node_ used to get node size and anchor - _Prefab node_ used to get node size and anchor
- You can point *position_function* for animations with _static_grid:set_position_function(node, pos)_ callback. Default - *gui.set_position()* - You can point *position_function* for animations with _static_grid:set_position_function(node, pos)_ callback. Default - *gui.set_position()*
## Dynamic Grid
[Dynamic Grid API here](https://insality.github.io/druid/modules/DynamicGrid.html)
### Overview
Component for manage node positions with different node sizes.
Unlike Static Grid, Dynamic Grid can place nodes only in one row or in one column.
Dynamic Grid can't have gaps between elements
- you will get error, if try spawn element far away from others.
Dynamic Grid should have __West__, __East__, __South__ or __North__ pivot (vertical or horizontal element placement)
### Setup
Create component with druid: `grid = druid:new_dynamic_grid(parent_node)`
Check the _parent_node_ have correct pivot point. You will get the error otherwise.
### Notes
- On _add node_ grid will set node parent to _parent_node_
- You can get array of position of every element for setup points of interest in scroll component
- You can get size of all elements for setup size in scroll component
- You can also bind the grid to the scroll component for auto resize scroll content size
- Pivot of parent_node matter for node placement
- You can point *position_function* for animations with _static_grid:set_position_function(node, pos)_ callback. Default - *gui.set_position()*
- First node placed at Grid pivot point. Other nodes placed nearby of other nodes.
- On *add/remove* nodes always shifted. You can point the shift side in this functions (*is_shift_left* boolean argumentp
## Data List ## Data List
[Data List API here](https://insality.github.io/druid/modules/DataList.html) [Data List API here](https://insality.github.io/druid/modules/DataList.html)

View File

@@ -148,7 +148,6 @@ Available keywords:
- `text`: Adds a [Druid Text](01-components.md#text) component. - `text`: Adds a [Druid Text](01-components.md#text) component.
- `lang_text`: Adds a [Druid Lang Text](01-components.md#lang-text) component. - `lang_text`: Adds a [Druid Lang Text](01-components.md#lang-text) component.
- `grid` or `static_grid`: Adds a [Druid Static Grid](01-components.md#static-grid) component. You should set up the Grid prefab for this component after generating the file. - `grid` or `static_grid`: Adds a [Druid Static Grid](01-components.md#static-grid) component. You should set up the Grid prefab for this component after generating the file.
- `dynamic_grid`: Adds a [Druid Dynamic Grid](01-components.md#dynamic-grid) component.
- `scroll_view`: Adds a [Druid Scroll](01-components.md#scroll) component. It also adds a `scroll_content` node with the same postfix. Ensure that it's the correct node. - `scroll_view`: Adds a [Druid Scroll](01-components.md#scroll) component. It also adds a `scroll_content` node with the same postfix. Ensure that it's the correct node.
- `blocker`: Adds a [Druid Blocker](01-components.md#blocker) component. - `blocker`: Adds a [Druid Blocker](01-components.md#blocker) component.
- `slider`: Adds a [Druid Slider](01-components.md#slider) component. You should adjust the end position of the Slider after generating the file. - `slider`: Adds a [Druid Slider](01-components.md#slider) component. You should adjust the end position of the Slider after generating the file.

View File

@@ -61,29 +61,6 @@ no_auto_input = 1
``` ```
## Template Name Check
By default, **Druid** automatically checks the parent component's template name to construct the full template name for the component. It's used in user custom components.
If, for some reason, you want to pass the full template name manually, you can disable this feature by setting the `druid.no_auto_template` field in the _game.project_ file:
```
[druid]
no_auto_template = 1
```
## Stencil Check
When creating input components inside stencil nodes, **Druid** automatically sets up `component:set_click_zone()` during the _late_init_ component step to restrict input clicks outside of the stencil zone. This is particularly useful for buttons inside scroll stencil nodes.
To disable this feature, add the following field to your _game.project_ file:
```
[druid]
no_stencil_check = 1
```
## Code Bindings ## Code Bindings
Adjust **Druid** settings as needed: Adjust **Druid** settings as needed:
@@ -121,7 +98,7 @@ window.set_listener(on_window_callback)
``` ```
## EmmyLua Annotations ## Lua Annotations
[EmmyLua](https://emmylua.github.io/annotation.html) is a Lua annotation library. It is a useful tool for enabling Lua code autocompletion in editors such as [VSCode](https://github.com/EmmyLua/VSCode-EmmyLua) and [IntelliJ IDEA](https://github.com/EmmyLua/IntelliJ-EmmyLua). [EmmyLua](https://emmylua.github.io/annotation.html) is a Lua annotation library. It is a useful tool for enabling Lua code autocompletion in editors such as [VSCode](https://github.com/EmmyLua/VSCode-EmmyLua) and [IntelliJ IDEA](https://github.com/EmmyLua/IntelliJ-EmmyLua).

View File

View File

@@ -0,0 +1,74 @@
# Quick API Reference
## Druid
```lua
local druid = require("druid.druid")
druid.init_window_listener()
druid.on_language_change()
druid.on_window_callback(window_event)
druid.set_default_style(style)
druid.set_sound_function(callback)
druid.set_text_function(callback)
self.druid = druid.new(context, [style])
```
## Druid Instance
```lua
self.druid:final()
self.druid:update(dt)
self.druid:on_input(action_id, action)
self.druid:on_message(message_id, message, sender)
self.druid:new(component, ...)
self.druid:new_back_handler([callback], [params])
self.druid:new_blocker(node)
self.druid:new_button(node, [callback], [params], [anim_node])
self.druid:new_container(node, [mode], [callback])
self.druid:new_data_list(druid_scroll, druid_grid, create_function)
self.druid:new_drag(node, [on_drag_callback])
self.druid:new_grid(parent_node, item, [in_row])
self.druid:new_hotkey(keys_array, [callback], [callback_argument])
self.druid:new_hover(node, [on_hover_callback], [on_mouse_hover_callback])
self.druid:new_input(click_node, text_node, [keyboard_type])
self.druid:new_lang_text(node, [locale_id], [adjust_type])
self.druid:new_layout(node, [mode])
self.druid:new_progress(node, key, [init_value])
self.druid:new_rich_input(template, [nodes])
self.druid:new_rich_text(text_node, [value])
self.druid:new_scroll(view_node, content_node)
self.druid:new_slider(pin_node, end_pos, [callback])
self.druid:new_swipe(node, [on_swipe_callback])
self.druid:new_text(node, [value], [no_adjust])
self.druid:new_timer(node, [seconds_from], [seconds_to], [callback])
self.druid:new_widget(widget, [template], [nodes], ...)
self.druid:on_window_event([window_event])
self.druid:remove(component)
self.druid:set_blacklist(blacklist_components)
self.druid:set_whitelist(whitelist_components)
```
## Components
### Base Component
### Blocker
### Button
### Container
### Data List
### Drag
### Grid
### Hotkey
### Hover
### Input
### Lang Text
### Layout
### Progress
### Rich Input
### Rich Text
### Scroll
### Slider
### Swipe
### Text
### Timer
## Helper

File diff suppressed because it is too large Load Diff

View File

@@ -1,65 +1,24 @@
-- Copyright (c) 2023 Maksim Tuprikov <insality@gmail.com>. This code is licensed under MIT license local event = require("event.event")
--- Component with event on back and backspace button.
-- <b># Overview #</b>
--
-- Back Handler is recommended to put in every game window to close it
-- or in main screen to call settings window.
--
-- <b># Notes #</b>
--
-- • Back Handler inheritance @{BaseComponent}, you can use all of its methods in addition to those described here.
--
-- • Back Handler react on release action ACTION_BACK or ACTION_BACKSPACE
-- @usage
-- local callback = function(self, params) ... end
--
-- local params = {}
-- local back_handler = self.druid:new_back_handler(callback, [params])
-- @module BackHandler
-- @within BaseComponent
-- @alias druid.back_handler
--- The @{DruidEvent} Event on back handler action.
--
-- Trigger on input action ACTION_BACK or ACTION_BACKSPACE
-- @usage
-- -- Subscribe additional callbacks:
-- back_handler.on_back:subscribe(callback)
-- @tfield DruidEvent on_back @{DruidEvent}
--- Custom args to pass in the callback
-- @usage
-- -- Replace params on runtime:
-- back_handler.params = { ... }
-- @tfield any|nil params
---
local Event = require("druid.event")
local const = require("druid.const") local const = require("druid.const")
local component = require("druid.component") local component = require("druid.component")
local BackHandler = component.create("back_handler") ---@class druid.back_handler: druid.base_component
---@field on_back event Trigger on back handler action, fun(self, params)
---@field params any|nil Custom args to pass in the callback
local M = component.create("back_handler")
--- The @{BackHandler} constructor ---@param callback function|nil
-- @tparam BackHandler self @{BackHandler} ---@param params any|nil
-- @tparam function callback @The callback(self, custom_args) to call on back event function M:init(callback, params)
-- @tparam any|nil custom_args Button events custom arguments self.params = params
-- @local self.on_back = event.create(callback)
function BackHandler.init(self, callback, custom_args)
self.params = custom_args
self.on_back = Event(callback)
end end
--- Component input handler ---@param action_id string
-- @tparam BackHandler self @{BackHandler} ---@param action table
-- @tparam string action_id on_input action id function M:on_input(action_id, action)
-- @tparam table action on_input action
-- @local
function BackHandler.on_input(self, action_id, action)
if not action.released then if not action.released then
return false return false
end end
@@ -73,4 +32,4 @@ function BackHandler.on_input(self, action_id, action)
end end
return BackHandler return M

View File

@@ -1,50 +1,22 @@
-- Copyright (c) 2023 Maksim Tuprikov <insality@gmail.com>. This code is licensed under MIT license
--- Component to consume input in special zone defined by GUI node.
-- <b># Overview #</b>
--
-- <b># Notes #</b>
--
-- Blocker consume input if `gui.pick_node` works on it.
--
-- • Blocker inheritance @{BaseComponent}, you can use all of its methods in addition to those described here.
--
-- • Blocker initial enabled state is `gui.is_enabled(node, true)`
--
-- • The Blocker node should be enabled to capture the input
-- @usage
-- local node = gui.get_node("blocker_node")
-- local blocker = self.druid:new_blocker(node)
-- @module Blocker
-- @within BaseComponent
-- @alias druid.blocker
---Blocker node
-- @tfield node node
---
local const = require("druid.const") local const = require("druid.const")
local component = require("druid.component") local component = require("druid.component")
local Blocker = component.create("blocker") ---@class druid.blocker: druid.base_component
---@field node node
---@field private _is_enabled boolean
local M = component.create("blocker")
--- The @{Blocker} constructor ---@param node node
-- @tparam Blocker self @{Blocker} function M:init(node)
-- @tparam node node Gui node
function Blocker.init(self, node)
self.node = self:get_node(node) self.node = self:get_node(node)
self._is_enabled = gui.is_enabled(self.node, true) self._is_enabled = gui.is_enabled(self.node, true)
end end
--- Component input handler ---@param action_id string
-- @tparam Blocker self @{Blocker} ---@param action table
-- @tparam string action_id on_input action id function M:on_input(action_id, action)
-- @tparam table action on_input action
-- @local
function Blocker.on_input(self, action_id, action)
if action_id ~= const.ACTION_TOUCH and if action_id ~= const.ACTION_TOUCH and
action_id ~= const.ACTION_MULTITOUCH and action_id ~= const.ACTION_MULTITOUCH and
action_id ~= nil then action_id ~= nil then
@@ -67,22 +39,21 @@ function Blocker.on_input(self, action_id, action)
end end
--- Set enabled blocker component state. ---Set blocker enabled state
-- ---@param state boolean
-- Don't change node enabled state itself. ---@return druid.blocker self
-- @tparam Blocker self @{Blocker} function M:set_enabled(state)
-- @tparam boolean|nil state Enabled state
function Blocker.set_enabled(self, state)
self._is_enabled = state self._is_enabled = state
return self
end end
--- Return blocker enabled state ---Get blocker enabled state
-- @tparam Blocker self @{Blocker} ---@return boolean
-- @treturn boolean @True, if blocker is enabled function M:is_enabled()
function Blocker.is_enabled(self)
return self._is_enabled return self._is_enabled
end end
return Blocker return M

View File

@@ -35,16 +35,16 @@
-- @alias druid.button -- @alias druid.button
--- The @{DruidEvent}: Event on successful release action over button. --- The event: Event on successful release action over button.
-- @usage -- @usage
-- -- Custom args passed in Button constructor -- -- Custom args passed in Button constructor
-- button.on_click:subscribe(function(self, custom_args, button_instance) -- button.on_click:subscribe(function(self, custom_args, button_instance)
-- print("On button click!") -- print("On button click!")
-- end) -- end)
-- @tfield DruidEvent on_click @{DruidEvent} -- @tfield event on_click event
--- The @{DruidEvent}: Event on repeated action over button. --- The event: Event on repeated action over button.
-- --
-- This callback will be triggered if user hold the button. The repeat rate pick from `input.repeat_interval` in game.project -- This callback will be triggered if user hold the button. The repeat rate pick from `input.repeat_interval` in game.project
-- @usage -- @usage
@@ -52,10 +52,10 @@
-- button.on_repeated_click:subscribe(function(self, custom_args, button_instance, click_count) -- button.on_repeated_click:subscribe(function(self, custom_args, button_instance, click_count)
-- print("On repeated Button click!") -- print("On repeated Button click!")
-- end) -- end)
-- @tfield DruidEvent on_repeated_click @{DruidEvent} -- @tfield event on_repeated_click event
--- The @{DruidEvent}: Event on long tap action over button. --- The event: Event on long tap action over button.
-- --
-- This callback will be triggered if user pressed the button and hold the some amount of time. -- This callback will be triggered if user pressed the button and hold the some amount of time.
-- The amount of time picked from button style param: LONGTAP_TIME -- The amount of time picked from button style param: LONGTAP_TIME
@@ -64,10 +64,10 @@
-- button.on_long_click:subscribe(function(self, custom_args, button_instance, hold_time) -- button.on_long_click:subscribe(function(self, custom_args, button_instance, hold_time)
-- print("On long Button click!") -- print("On long Button click!")
-- end) -- end)
-- @tfield DruidEvent on_long_click @{DruidEvent} -- @tfield event on_long_click event
--- The @{DruidEvent}: Event on double tap action over button. --- The event: Event on double tap action over button.
-- --
-- If secondary click was too fast after previous one, the double -- If secondary click was too fast after previous one, the double
-- click will be called instead usual click (if on_double_click subscriber exists) -- click will be called instead usual click (if on_double_click subscriber exists)
@@ -76,10 +76,10 @@
-- button.on_double_click:subscribe(function(self, custom_args, button_instance, click_amount) -- button.on_double_click:subscribe(function(self, custom_args, button_instance, click_amount)
-- print("On double Button click!") -- print("On double Button click!")
-- end) -- end)
-- @tfield DruidEvent on_double_click @{DruidEvent} -- @tfield event on_double_click event
--- The @{DruidEvent}: Event calls every frame before on_long_click event. --- The event: Event calls every frame before on_long_click event.
-- --
-- If long_click subscriber exists, the on_hold_callback will be called before long_click trigger. -- If long_click subscriber exists, the on_hold_callback will be called before long_click trigger.
-- --
@@ -89,10 +89,10 @@
-- button.on_double_click:subscribe(function(self, custom_args, button_instance, time) -- button.on_double_click:subscribe(function(self, custom_args, button_instance, time)
-- print("On hold Button callback!") -- print("On hold Button callback!")
-- end) -- end)
-- @tfield DruidEvent on_hold_callback @{DruidEvent} -- @tfield event on_hold_callback event
--- The @{DruidEvent}: Event calls if click event was outside of button. --- The event: Event calls if click event was outside of button.
-- --
-- This event will be triggered for each button what was not clicked on user click action -- This event will be triggered for each button what was not clicked on user click action
-- --
@@ -102,16 +102,16 @@
-- button.on_click_outside:subscribe(function(self, custom_args, button_instance) -- button.on_click_outside:subscribe(function(self, custom_args, button_instance)
-- print("On click Button outside!") -- print("On click Button outside!")
-- end) -- end)
-- @tfield DruidEvent on_click_outside @{DruidEvent} -- @tfield event on_click_outside event
--- The @{DruidEvent}: Event triggered if button was pressed by user. --- The event: Event triggered if button was pressed by user.
-- @usage -- @usage
-- -- Custom args passed in Button constructor -- -- Custom args passed in Button constructor
-- button.on_pressed:subscribe(function(self, custom_args, button_instance) -- button.on_pressed:subscribe(function(self, custom_args, button_instance)
-- print("On Button pressed!") -- print("On Button pressed!")
-- end) -- end)
-- @tfield DruidEvent on_pressed @{DruidEvent} -- @tfield event on_pressed event
--- Button trigger node --- Button trigger node
-- @tfield node node -- @tfield node node
@@ -128,20 +128,40 @@
---Custom args for any Button event. Setup in Button constructor ---Custom args for any Button event. Setup in Button constructor
-- @tfield any params -- @tfield any params
--- The @{Hover}: Button Hover component --- The Hover: Button Hover component
-- @tfield Hover hover @{Hover} -- @tfield Hover hover Hover
--- Additional button click area, defined by another GUI node --- Additional button click area, defined by another GUI node
-- @tfield node|nil click_zone -- @tfield node|nil click_zone
--- ---
local Event = require("druid.event") local event = require("event.event")
local const = require("druid.const") local const = require("druid.const")
local helper = require("druid.helper") local helper = require("druid.helper")
local component = require("druid.component") local component = require("druid.component")
local Button = component.create("button") ---Clickable node with various interaction callbacks
---@class druid.button: druid.base_component
---@field on_click event function(self, custom_args, button_instance)
---@field on_pressed event
---@field on_repeated_click event
---@field on_long_click event
---@field on_double_click event
---@field on_hold_callback event
---@field on_click_outside event
---@field node node
---@field node_id hash
---@field anim_node node
---@field params any
---@field hover druid.hover
---@field click_zone node|nil
---@field start_scale vector3
---@field start_pos vector3
---@field disabled boolean
---@field key_trigger hash
---@field style table
local M = component.create("button")
local function is_input_match(self, action_id) local function is_input_match(self, action_id)
@@ -210,6 +230,7 @@ local function on_button_hold(self, press_time)
end end
---@param self druid.button
local function on_button_release(self) local function on_button_release(self)
if self.is_repeated_started then if self.is_repeated_started then
return false return false
@@ -234,10 +255,10 @@ local function on_button_release(self)
local time = socket.gettime() local time = socket.gettime()
local is_long_click = (time - self.last_pressed_time) >= self.style.LONGTAP_TIME local is_long_click = (time - self.last_pressed_time) >= self.style.LONGTAP_TIME
is_long_click = is_long_click and self.on_long_click:is_exist() is_long_click = is_long_click and not self.on_long_click:is_empty()
local is_double_click = (time - self.last_released_time) < self.style.DOUBLETAP_TIME local is_double_click = (time - self.last_released_time) < self.style.DOUBLETAP_TIME
is_double_click = is_double_click and self.on_double_click:is_exist() is_double_click = is_double_click and not self.on_double_click:is_empty()
if is_long_click then if is_long_click then
local is_hold_complete = (time - self.last_pressed_time) >= self.style.AUTOHOLD_TRIGGER local is_hold_complete = (time - self.last_pressed_time) >= self.style.AUTOHOLD_TRIGGER
@@ -260,18 +281,20 @@ end
--- Component style params. --- Component style params.
-- You can override this component styles params in Druid styles table ---You can override this component styles params in Druid styles table
-- or create your own style ---or create your own style
-- @table style ---@class druid.button.style
-- @tfield number|nil LONGTAP_TIME Minimum time to trigger on_hold_callback. Default: 0.4 ---@field LONGTAP_TIME number|nil Minimum time to trigger on_hold_callback. Default: 0.4
-- @tfield number|nil AUTOHOLD_TRIGGER Maximum hold time to trigger button release while holding. Default: 0.8 ---@field AUTOHOLD_TRIGGER number|nil Maximum hold time to trigger button release while holding. Default: 0.8
-- @tfield number|nil DOUBLETAP_TIME Time between double taps. Default: 0.4 ---@field DOUBLETAP_TIME number|nil Time between double taps. Default: 0.4
-- @tfield function on_click function(self, node) ---@field on_click fun(self, node)|nil
-- @tfield function on_click_disabled function(self, node) ---@field on_click_disabled fun(self, node)|nil
-- @tfield function on_hover function(self, node, hover_state) ---@field on_hover fun(self, node, hover_state)|nil
-- @tfield function on_mouse_hover function(self, node, hover_state) ---@field on_mouse_hover fun(self, node, hover_state)|nil
-- @tfield function on_set_enabled function(self, node, enabled_state) ---@field on_set_enabled fun(self, node, enabled_state)|nil
function Button.on_style_change(self, style)
---@param style druid.button.style
function M:on_style_change(style)
self.style = {} self.style = {}
self.style.LONGTAP_TIME = style.LONGTAP_TIME or 0.4 self.style.LONGTAP_TIME = style.LONGTAP_TIME or 0.4
self.style.AUTOHOLD_TRIGGER = style.AUTOHOLD_TRIGGER or 0.8 self.style.AUTOHOLD_TRIGGER = style.AUTOHOLD_TRIGGER or 0.8
@@ -285,22 +308,21 @@ function Button.on_style_change(self, style)
end end
--- The @{Button} constructor ---Button constructor
-- @tparam Button self @{Button} ---@param node_or_node_id node|string Node name or GUI Node itself.
-- @tparam string|node node The node_id or gui.get_node(node_id) ---@param callback fun()|nil Callback on button click
-- @tparam function callback On click button callback ---@param custom_args any|nil Custom args for any Button event
-- @tparam any|nil custom_args Button events custom arguments ---@param anim_node node|string|nil Node to animate instead of trigger node.
-- @tparam string|node|nil anim_node Node to animate instead of trigger node. function M:init(node_or_node_id, callback, custom_args, anim_node)
function Button.init(self, node, callback, custom_args, anim_node)
self.druid = self:get_druid() self.druid = self:get_druid()
self.node = self:get_node(node) self.node = self:get_node(node_or_node_id)
self.node_id = gui.get_id(self.node) self.node_id = gui.get_id(self.node)
self.anim_node = anim_node and self:get_node(anim_node) or self.node self.anim_node = anim_node and self:get_node(anim_node) or self.node
self.start_scale = gui.get_scale(self.anim_node) self.start_scale = gui.get_scale(self.anim_node)
self.start_pos = gui.get_position(self.anim_node) self.start_pos = gui.get_position(self.anim_node)
self.params = custom_args self.params = custom_args
self.hover = self.druid:new_hover(node, on_button_hover) self.hover = self.druid:new_hover(node_or_node_id, on_button_hover)
self.hover.on_mouse_hover:subscribe(on_button_mouse_hover) self.hover.on_mouse_hover:subscribe(on_button_mouse_hover)
self.click_zone = nil self.click_zone = nil
self.is_repeated_started = false self.is_repeated_started = false
@@ -315,18 +337,18 @@ function Button.init(self, node, callback, custom_args, anim_node)
self._is_html5_listener_set = false self._is_html5_listener_set = false
-- Events -- Events
self.on_click = Event(callback) self.on_click = event.create(callback)
self.on_pressed = Event() self.on_pressed = event.create()
self.on_repeated_click = Event() self.on_repeated_click = event.create()
self.on_long_click = Event() self.on_long_click = event.create()
self.on_double_click = Event() self.on_double_click = event.create()
self.on_hold_callback = Event() self.on_hold_callback = event.create()
self.on_click_outside = Event() self.on_click_outside = event.create()
end end
function Button.on_late_init(self) function M:on_late_init()
if not self.click_zone and const.IS_STENCIL_CHECK then if not self.click_zone then
local stencil_node = helper.get_closest_stencil_node(self.node) local stencil_node = helper.get_closest_stencil_node(self.node)
if stencil_node then if stencil_node then
self:set_click_zone(stencil_node) self:set_click_zone(stencil_node)
@@ -335,7 +357,7 @@ function Button.on_late_init(self)
end end
function Button.on_input(self, action_id, action) function M:on_input(action_id, action)
if not is_input_match(self, action_id) then if not is_input_match(self, action_id) then
return false return false
end end
@@ -388,7 +410,7 @@ function Button.on_input(self, action_id, action)
-- While hold button, repeat rate pick from input.repeat_interval -- While hold button, repeat rate pick from input.repeat_interval
if action.repeated then if action.repeated then
if self.on_repeated_click:is_exist() and self.can_action then if not self.on_repeated_click:is_empty() and self.can_action then
on_button_repeated_click(self) on_button_repeated_click(self)
return is_consume return is_consume
end end
@@ -398,7 +420,7 @@ function Button.on_input(self, action_id, action)
return on_button_release(self) and is_consume return on_button_release(self) and is_consume
end end
if self.can_action and self.on_long_click:is_exist() then if self.can_action and not self.on_long_click:is_empty() then
local press_time = socket.gettime() - self.last_pressed_time local press_time = socket.gettime() - self.last_pressed_time
if self.style.AUTOHOLD_TRIGGER <= press_time then if self.style.AUTOHOLD_TRIGGER <= press_time then
@@ -416,48 +438,19 @@ function Button.on_input(self, action_id, action)
end end
function Button.on_input_interrupt(self) function M:on_input_interrupt()
self.can_action = false self.can_action = false
self.hover:set_hover(false) self.hover:set_hover(false)
self.hover:set_mouse_hover(false) self.hover:set_mouse_hover(false)
end end
function Button.on_message_input(self, node_id, message)
if node_id ~= self.node_id or self.disabled or not gui.is_enabled(self.node) then
return false
end
if message.action == const.MESSAGE_INPUT.BUTTON_CLICK then
on_button_click(self)
end
if message.action == const.MESSAGE_INPUT.BUTTON_LONG_CLICK then
on_button_long_click(self)
end
if message.action == const.MESSAGE_INPUT.BUTTON_DOUBLE_CLICK then
on_button_double_click(self)
end
if message.action == const.MESSAGE_INPUT.BUTTON_REPEATED_CLICK then
on_button_repeated_click(self)
self.is_repeated_started = false
self.last_pressed_time = socket.gettime()
end
end
--- Set button enabled state. --- Set button enabled state.
-- The style.on_set_enabled will be triggered. -- The style.on_set_enabled will be triggered.
-- Disabled button is not clickable. -- Disabled button is not clickable.
-- @tparam Button self @{Button} ---@param state boolean|nil Enabled state
-- @tparam boolean|nil state Enabled state ---@return druid.button self
-- @treturn Button Current button instance function M:set_enabled(state)
-- @usage
-- button:set_enabled(false)
-- button:set_enabled(true)
function Button.set_enabled(self, state)
self.disabled = not state self.disabled = not state
self.hover:set_enabled(state) self.hover:set_enabled(state)
self.style.on_set_enabled(self, self.node, state) self.style.on_set_enabled(self, self.node, state)
@@ -469,11 +462,8 @@ end
--- Get button enabled state. --- Get button enabled state.
-- --
-- By default all Buttons is enabled on creating. -- By default all Buttons is enabled on creating.
-- @tparam Button self @{Button} ---@return boolean @True, if button is enabled now, False overwise
-- @treturn boolean @True, if button is enabled now, False overwise function M:is_enabled()
-- @usage
-- local is_enabled = button:is_enabled()
function Button.is_enabled(self)
return not self.disabled return not self.disabled
end end
@@ -482,68 +472,61 @@ end
-- Useful to restrict click outside out stencil node or scrollable content. -- Useful to restrict click outside out stencil node or scrollable content.
-- --
-- This functions calls automatically if you don't disable it in game.project: druid.no_stencil_check -- This functions calls automatically if you don't disable it in game.project: druid.no_stencil_check
-- @tparam Button self @{Button} ---@param zone node|string|nil Gui node
-- @tparam node|string|nil zone Gui node ---@return druid.button self
-- @treturn Button Current button instance function M:set_click_zone(zone)
-- @usage self.click_zone = zone and self:get_node(zone) or nil
-- button:set_click_zone("stencil_node")
function Button.set_click_zone(self, zone)
self.click_zone = self:get_node(zone)
self.hover:set_click_zone(zone) self.hover:set_click_zone(zone)
return self return self
end end
--- Set key name to trigger this button by keyboard. ---Set key name to trigger this button by keyboard.
-- @tparam Button self @{Button} ---@param key hash|string The action_id of the input key. Example: "key_space"
-- @tparam hash|string key The action_id of the input key ---@return druid.button self
-- @treturn Button Current button instance function M:set_key_trigger(key)
-- @usage if type(key) == "string" then
-- button:set_key_trigger("key_space")
function Button.set_key_trigger(self, key)
self.key_trigger = hash(key) self.key_trigger = hash(key)
else
self.key_trigger = key
end
return self return self
end end
--- Get current key name to trigger this button. --- Get current key name to trigger this button.
-- @tparam Button self ---@return hash key_trigger The action_id of the input key
-- @treturn hash The action_id of the input key function M:get_key_trigger()
-- @usage
-- local key_hash = button:get_key_trigger()
function Button.get_key_trigger(self)
return self.key_trigger return self.key_trigger
end end
--- Set function for additional check for button click availability ---Set function for additional check for button click availability
-- @tparam Button self ---@param check_function function|nil Should return true or false. If true - button can be pressed.
-- @tparam function|nil check_function Should return true or false. If true - button can be pressed. ---@param failure_callback function|nil Function will be called on button click, if check function return false
-- @tparam function|nil failure_callback Function will be called on button click, if check function return false ---@return druid.button self
-- @treturn Button Current button instance function M:set_check_function(check_function, failure_callback)
function Button.set_check_function(self, check_function, failure_callback)
self._check_function = check_function self._check_function = check_function
self._failure_callback = failure_callback self._failure_callback = failure_callback
return self
end end
--- Set Button mode to work inside user HTML5 interaction event. ---Set Button mode to work inside user HTML5 interaction event.
-- ---
-- It's required to make protected things like copy & paste text, show mobile keyboard, etc ---It's required to make protected things like copy & paste text, show mobile keyboard, etc
-- The HTML5 button's doesn't call any events except on_click event. ---The HTML5 button's doesn't call any events except on_click event.
-- ---
-- If the game is not HTML, html mode will be not enabled ---If the game is not HTML, html mode will be not enabled
-- @tparam Button self ---@param is_web_mode boolean|nil If true - button will be called inside html5 callback
-- @tparam boolean|nil is_web_mode If true - button will be called inside html5 callback ---@return druid.button self
-- @treturn Button Current button instance function M:set_web_user_interaction(is_web_mode)
-- @usage
-- button:set_web_user_interaction(true)
function Button.set_web_user_interaction(self, is_web_mode)
self._is_html5_mode = not not (is_web_mode and html5) self._is_html5_mode = not not (is_web_mode and html5)
return self return self
end end
return Button return M

View File

@@ -14,19 +14,19 @@
-- @tfield node node -- @tfield node node
--- Event on touch start callback(self) --- Event on touch start callback(self)
-- @tfield DruidEvent on_touch_start @{DruidEvent} -- @tfield event on_touch_start event
--- Event on touch end callback(self) --- Event on touch end callback(self)
-- @tfield DruidEvent on_touch_end @{DruidEvent} -- @tfield event on_touch_end event
--- Event on drag start callback(self, touch) --- Event on drag start callback(self, touch)
-- @tfield DruidEvent on_drag_start @{DruidEvent} -- @tfield event on_drag_start event
--- on drag progress callback(self, dx, dy, total_x, total_y, touch) --- on drag progress callback(self, dx, dy, total_x, total_y, touch)
-- @tfield DruidEvent on_drag Event @{DruidEvent} -- @tfield event on_drag Event event
--- Event on drag end callback(self, total_x, total_y, touch) --- Event on drag end callback(self, total_x, total_y, touch)
-- @tfield DruidEvent on_drag_end @{DruidEvent} -- @tfield event on_drag_end event
--- Is component now touching --- Is component now touching
-- @tfield boolean is_touch -- @tfield boolean is_touch
@@ -57,12 +57,40 @@
--- ---
local Event = require("druid.event") local event = require("event.event")
local const = require("druid.const") local const = require("druid.const")
local helper = require("druid.helper") local helper = require("druid.helper")
local component = require("druid.component") local component = require("druid.component")
local Drag = component.create("drag", const.PRIORITY_INPUT_HIGH) ---@class druid.drag.style
---@field DRAG_DEADZONE number Distance in pixels to start dragging. Default: 10
---@field NO_USE_SCREEN_KOEF boolean If screen aspect ratio affects on drag values. Default: false
---@class druid.drag: druid.base_component
---@field node node
---@field on_touch_start event
---@field on_touch_end event
---@field on_drag_start event
---@field on_drag event
---@field on_drag_end event
---@field style druid.drag.style
---@field click_zone node|nil
---@field is_touch boolean
---@field is_drag boolean
---@field can_x boolean
---@field can_y boolean
---@field dx number
---@field dy number
---@field touch_id number
---@field x number
---@field y number
---@field screen_x number
---@field screen_y number
---@field touch_start_pos vector3
---@field private _is_enabled boolean
---@field private _x_koef number
---@field private _y_koef number
local M = component.create("drag", const.PRIORITY_INPUT_HIGH)
local function start_touch(self, touch) local function start_touch(self, touch)
@@ -177,19 +205,21 @@ end
-- @table style -- @table style
-- @tfield number|nil DRAG_DEADZONE Distance in pixels to start dragging. Default: 10 -- @tfield number|nil DRAG_DEADZONE Distance in pixels to start dragging. Default: 10
-- @tfield boolean|nil NO_USE_SCREEN_KOEF If screen aspect ratio affects on drag values. Default: false -- @tfield boolean|nil NO_USE_SCREEN_KOEF If screen aspect ratio affects on drag values. Default: false
function Drag.on_style_change(self, style) function M:on_style_change(style)
self.style = {} self.style = {
self.style.DRAG_DEADZONE = style.DRAG_DEADZONE or 10 DRAG_DEADZONE = style.DRAG_DEADZONE or 10,
self.style.NO_USE_SCREEN_KOEF = style.NO_USE_SCREEN_KOEF or false NO_USE_SCREEN_KOEF = style.NO_USE_SCREEN_KOEF or false,
}
end end
--- The @{Drag} constructor ---Drag constructor
-- @tparam Drag self @{Drag} ---@param node_or_node_id node|string
-- @tparam node node GUI node to detect dragging ---@param on_drag_callback function
-- @tparam function on_drag_callback Callback for on_drag_event(self, dx, dy) function M:init(node_or_node_id, on_drag_callback)
function Drag.init(self, node, on_drag_callback) self.druid = self:get_druid()
self.node = self:get_node(node) self.node = self:get_node(node_or_node_id)
self.hover = self.druid:new_hover(self.node)
self.dx = 0 self.dx = 0
self.dy = 0 self.dy = 0
@@ -209,18 +239,32 @@ function Drag.init(self, node, on_drag_callback)
self._scene_scale = helper.get_scene_scale(self.node) self._scene_scale = helper.get_scene_scale(self.node)
self.click_zone = nil self.click_zone = nil
self.on_touch_start = Event() self.on_touch_start = event.create()
self.on_touch_end = Event() self.on_touch_end = event.create()
self.on_drag_start = Event() self.on_drag_start = event.create()
self.on_drag = Event(on_drag_callback) self.on_drag = event.create(on_drag_callback)
self.on_drag_end = Event() self.on_drag_end = event.create()
self:on_window_resized() self:on_window_resized()
self:set_drag_cursors(true)
end end
function Drag.on_late_init(self) ---Set Drag component enabled state.
if not self.click_zone and const.IS_STENCIL_CHECK then ---@param is_enabled boolean
function M:set_drag_cursors(is_enabled)
if defos and is_enabled then
self.hover.style.ON_HOVER_CURSOR = defos.CURSOR_CROSSHAIR
self.hover.style.ON_MOUSE_HOVER_CURSOR = defos.CURSOR_HAND
else
self.hover.style.ON_HOVER_CURSOR = nil
self.hover.style.ON_MOUSE_HOVER_CURSOR = nil
end
end
function M:on_late_init()
if not self.click_zone then
local stencil_node = helper.get_closest_stencil_node(self.node) local stencil_node = helper.get_closest_stencil_node(self.node)
if stencil_node then if stencil_node then
self:set_click_zone(stencil_node) self:set_click_zone(stencil_node)
@@ -229,7 +273,7 @@ function Drag.on_late_init(self)
end end
function Drag.on_window_resized(self) function M:on_window_resized()
local x_koef, y_koef = helper.get_screen_aspect_koef() local x_koef, y_koef = helper.get_screen_aspect_koef()
self._x_koef = x_koef self._x_koef = x_koef
self._y_koef = y_koef self._y_koef = y_koef
@@ -237,14 +281,17 @@ function Drag.on_window_resized(self)
end end
function Drag.on_input_interrupt(self) function M:on_input_interrupt()
if self.is_drag or self.is_touch then if self.is_drag or self.is_touch then
end_touch(self) end_touch(self)
end end
end end
function Drag.on_input(self, action_id, action) ---@local
---@param action_id string
---@param action table
function M:on_input(action_id, action)
if action_id ~= const.ACTION_TOUCH and action_id ~= const.ACTION_MULTITOUCH then if action_id ~= const.ACTION_TOUCH and action_id ~= const.ACTION_MULTITOUCH then
return false return false
end end
@@ -321,29 +368,31 @@ function Drag.on_input(self, action_id, action)
end end
--- Strict drag click area. Useful for ---Set Drag click zone
-- restrict events outside stencil node ---@param node node|string|nil
-- @tparam Drag self @{Drag} ---@return druid.drag self Current instance
-- @tparam node|string|nil node Gui node function M:set_click_zone(node)
function Drag.set_click_zone(self, node) self.click_zone = node and self:get_node(node) or nil
self.click_zone = self:get_node(node)
return self
end end
--- Set Drag input enabled or disabled ---Set Drag component enabled state.
-- @tparam Drag self @{Drag} ---@param is_enabled boolean
-- @tparam boolean|nil is_enabled ---@return druid.drag self Current instance
function Drag.set_enabled(self, is_enabled) function M:set_enabled(is_enabled)
self._is_enabled = is_enabled self._is_enabled = is_enabled
return self
end end
--- Check if Drag component is enabled ---Check if Drag component is enabled
-- @tparam Drag self @{Drag} ---@return boolean
-- @treturn boolean function M:is_enabled()
function Drag.is_enabled(self)
return self._is_enabled return self._is_enabled
end end
return Drag return M

View File

@@ -9,27 +9,36 @@
-- @tfield node node -- @tfield node node
--- On hover callback(self, state, hover_instance) --- On hover callback(self, state, hover_instance)
-- @tfield DruidEvent on_hover @{DruidEvent} -- @tfield event on_hover event
--- On mouse hover callback(self, state, hover_instance) --- On mouse hover callback(self, state, hover_instance)
-- @tfield DruidEvent on_mouse_hover @{DruidEvent} -- @tfield event on_mouse_hover event
--- ---
local Event = require("druid.event") local event = require("event.event")
local const = require("druid.const") local const = require("druid.const")
local helper = require("druid.helper") local helper = require("druid.helper")
local component = require("druid.component") local component = require("druid.component")
local Hover = component.create("hover") ---@class druid.hover: druid.base_component
---@field node node
---@field on_hover event
---@field on_mouse_hover event
---@field style table
---@field click_zone node
---@field private _is_hovered boolean|nil
---@field private _is_mouse_hovered boolean|nil
---@field private _is_enabled boolean|nil
---@field private _is_mobile boolean
local M = component.create("hover")
--- The @{Hover} constructor --- The Hover constructor
-- @tparam Hover self @{Hover} ---@param node node Gui node
-- @tparam node node Gui node ---@param on_hover_callback function Hover callback
-- @tparam function on_hover_callback Hover callback ---@param on_mouse_hover function On mouse hover callback
-- @tparam function on_mouse_hover On mouse hover callback function M:init(node, on_hover_callback, on_mouse_hover)
function Hover.init(self, node, on_hover_callback, on_mouse_hover)
self.node = self:get_node(node) self.node = self:get_node(node)
self._is_hovered = false self._is_hovered = false
@@ -37,13 +46,13 @@ function Hover.init(self, node, on_hover_callback, on_mouse_hover)
self._is_enabled = true self._is_enabled = true
self._is_mobile = helper.is_mobile() self._is_mobile = helper.is_mobile()
self.on_hover = Event(on_hover_callback) self.on_hover = event.create(on_hover_callback)
self.on_mouse_hover = Event(on_mouse_hover) self.on_mouse_hover = event.create(on_mouse_hover)
end end
function Hover.on_late_init(self) function M:on_late_init()
if not self.click_zone and const.IS_STENCIL_CHECK then if not self.click_zone then
local stencil_node = helper.get_closest_stencil_node(self.node) local stencil_node = helper.get_closest_stencil_node(self.node)
if stencil_node then if stencil_node then
self:set_click_zone(stencil_node) self:set_click_zone(stencil_node)
@@ -58,14 +67,14 @@ end
-- @table style -- @table style
-- @tfield[opt] string ON_HOVER_CURSOR Mouse hover style on node hover -- @tfield[opt] string ON_HOVER_CURSOR Mouse hover style on node hover
-- @tfield[opt] string ON_MOUSE_HOVER_CURSOR Mouse hover style on node mouse hover -- @tfield[opt] string ON_MOUSE_HOVER_CURSOR Mouse hover style on node mouse hover
function Hover.on_style_change(self, style) function M:on_style_change(style)
self.style = {} self.style = {}
self.style.ON_HOVER_CURSOR = style.ON_HOVER_CURSOR or nil self.style.ON_HOVER_CURSOR = style.ON_HOVER_CURSOR or nil
self.style.ON_MOUSE_HOVER_CURSOR = style.ON_MOUSE_HOVER_CURSOR or nil self.style.ON_MOUSE_HOVER_CURSOR = style.ON_MOUSE_HOVER_CURSOR or nil
end end
function Hover.on_input(self, action_id, action) function M:on_input(action_id, action)
if action_id ~= const.ACTION_TOUCH and action_id ~= nil then if action_id ~= const.ACTION_TOUCH and action_id ~= nil then
return false return false
end end
@@ -99,15 +108,14 @@ function Hover.on_input(self, action_id, action)
end end
function Hover.on_input_interrupt(self) function M:on_input_interrupt()
self:set_hover(false) self:set_hover(false)
end end
--- Set hover state --- Set hover state
-- @tparam Hover self @{Hover} ---@param state boolean|nil The hover state
-- @tparam boolean|nil state The hover state function M:set_hover(state)
function Hover.set_hover(self, state)
if self._is_hovered == state then if self._is_hovered == state then
return return
end end
@@ -122,17 +130,15 @@ end
--- Return current hover state. True if touch action was on the node at current time --- Return current hover state. True if touch action was on the node at current time
-- @tparam Hover self @{Hover} ---@return boolean The current hovered state
-- @treturn boolean The current hovered state function M:is_hovered()
function Hover.is_hovered(self)
return self._is_hovered return self._is_hovered
end end
--- Set mouse hover state --- Set mouse hover state
-- @tparam Hover self @{Hover} ---@param state boolean|nil The mouse hover state
-- @tparam boolean|nil state The mouse hover state function M:set_mouse_hover(state)
function Hover.set_mouse_hover(self, state)
if self._is_mouse_hovered == state then if self._is_mouse_hovered == state then
return return
end end
@@ -147,18 +153,21 @@ end
--- Return current hover state. True if nil action_id (usually desktop mouse) was on the node at current time --- Return current hover state. True if nil action_id (usually desktop mouse) was on the node at current time
-- @tparam Hover self @{Hover} ---@return boolean The current hovered state
-- @treturn boolean The current hovered state function M:is_mouse_hovered()
function Hover.is_mouse_hovered(self)
return self._is_mouse_hovered return self._is_mouse_hovered
end end
--- Strict hover click area. Useful for --- Strict hover click area. Useful for
-- no click events outside stencil node -- no click events outside stencil node
-- @tparam Hover self @{Hover} ---@param zone node|string|nil Gui node
-- @tparam node|string|nil zone Gui node function M:set_click_zone(zone)
function Hover.set_click_zone(self, zone) if not zone then
self.click_zone = nil
return
end
self.click_zone = self:get_node(zone) self.click_zone = self:get_node(zone)
end end
@@ -166,9 +175,8 @@ end
--- Set enable state of hover component. --- Set enable state of hover component.
-- If hover is not enabled, it will not generate -- If hover is not enabled, it will not generate
-- any hover events -- any hover events
-- @tparam Hover self @{Hover} ---@param state boolean|nil The hover enabled state
-- @tparam boolean|nil state The hover enabled state function M:set_enabled(state)
function Hover.set_enabled(self, state)
self._is_enabled = state self._is_enabled = state
if not state then if not state then
@@ -183,16 +191,16 @@ end
--- Return current hover enabled state --- Return current hover enabled state
-- @tparam Hover self @{Hover} ---@return boolean The hover enabled state
-- @treturn boolean The hover enabled state function M:is_enabled()
function Hover.is_enabled(self)
return self._is_enabled return self._is_enabled
end end
-- Internal cursor stack -- Internal cursor stack
local cursor_stack = {} local cursor_stack = {}
function Hover:_set_cursor(priority, cursor) ---@local
function M:_set_cursor(priority, cursor)
if not defos then if not defos then
return return
end end
@@ -217,4 +225,4 @@ function Hover:_set_cursor(priority, cursor)
end end
return Hover return M

View File

@@ -39,13 +39,13 @@
--- On scroll move callback(self, position) --- On scroll move callback(self, position)
-- @tfield DruidEvent on_scroll @{DruidEvent} -- @tfield event on_scroll event
--- On scroll_to function callback(self, target, is_instant) --- On scroll_to function callback(self, target, is_instant)
-- @tfield DruidEvent on_scroll_to @{DruidEvent} -- @tfield event on_scroll_to event
--- On scroll_to_index function callback(self, index, point) --- On scroll_to_index function callback(self, index, point)
-- @tfield DruidEvent on_point_scroll @{DruidEvent} -- @tfield event on_point_scroll event
--- Scroll view node --- Scroll view node
-- @tfield node view_node -- @tfield node view_node
@@ -75,7 +75,7 @@
-- @tfield vector3 available_size -- @tfield vector3 available_size
--- Drag Druid component --- Drag Druid component
-- @tfield Drag drag @{Drag} -- @tfield Drag drag Drag
--- Current index of points of interests --- Current index of points of interests
-- @tfield number|nil selected -- @tfield number|nil selected
@@ -85,12 +85,37 @@
--- ---
local Event = require("druid.event") local event = require("event.event")
local const = require("druid.const") local const = require("druid.const")
local helper = require("druid.helper") local helper = require("druid.helper")
local component = require("druid.component") local component = require("druid.component")
local Scroll = component.create("scroll") ---@class druid.scroll: druid.base_component
---@field node node The root node
---@field click_zone node|nil Optional click zone to restrict scroll area
---@field on_scroll event Triggered on scroll move with (self, position)
---@field on_scroll_to event Triggered on scroll_to with (self, target, is_instant)
---@field on_point_scroll event Triggered on scroll_to_index with (self, index, point)
---@field view_node node The scroll view node (static part)
---@field view_border vector4 The scroll view borders
---@field content_node node The scroll content node (moving part)
---@field view_size vector3 Size of the view node
---@field position vector3 Current scroll position
---@field target_position vector3 Target scroll position for animations
---@field available_pos vector4 Available content position (min_x, max_y, max_x, min_y)
---@field available_size vector3 Size of available positions (width, height, 0)
---@field drag druid.drag The drag component instance
---@field selected number|nil Current selected point of interest index
---@field is_animate boolean True if scroll is animating
---@field private _is_inert boolean True if inertial scrolling is enabled
---@field private inertion vector3 Current inertial movement vector
---@field private _is_horizontal_scroll boolean True if horizontal scroll enabled
---@field private _is_vertical_scroll boolean True if vertical scroll enabled
---@field private _grid_on_change event Grid items change event
---@field private _grid_on_change_callback function Grid change callback
---@field private _offset vector3 Content start offset
---@field private style table Component style parameters
local M = component.create("scroll")
local function inverse_lerp(min, max, current) local function inverse_lerp(min, max, current)
@@ -138,7 +163,7 @@ end
-- @tfield boolean|nil WHEEL_SCROLL_SPEED The scroll speed via mouse wheel scroll or touchpad. Set to 0 to disable wheel scrolling. Default: 0 -- @tfield boolean|nil WHEEL_SCROLL_SPEED The scroll speed via mouse wheel scroll or touchpad. Set to 0 to disable wheel scrolling. Default: 0
-- @tfield boolean|nil WHEEL_SCROLL_INVERTED If true, invert direction for touchpad and mouse wheel scroll. Default: false -- @tfield boolean|nil WHEEL_SCROLL_INVERTED If true, invert direction for touchpad and mouse wheel scroll. Default: false
-- @tfield boolean|nil WHEEL_SCROLL_BY_INERTION If true, wheel will add inertion to scroll. Direct set position otherwise.. Default: false -- @tfield boolean|nil WHEEL_SCROLL_BY_INERTION If true, wheel will add inertion to scroll. Direct set position otherwise.. Default: false
function Scroll.on_style_change(self, style) function M:on_style_change(style)
self.style = {} self.style = {}
self.style.EXTRA_STRETCH_SIZE = style.EXTRA_STRETCH_SIZE or 0 self.style.EXTRA_STRETCH_SIZE = style.EXTRA_STRETCH_SIZE or 0
self.style.ANIM_SPEED = style.ANIM_SPEED or 0.2 self.style.ANIM_SPEED = style.ANIM_SPEED or 0.2
@@ -161,11 +186,10 @@ function Scroll.on_style_change(self, style)
end end
--- The @{Scroll} constructor --- The Scroll constructor
-- @tparam Scroll self @{Scroll} ---@param view_node string|node GUI view scroll node
-- @tparam string|node view_node GUI view scroll node ---@param content_node string|node GUI content scroll node
-- @tparam string|node content_node GUI content scroll node function M:init(view_node, content_node)
function Scroll.init(self, view_node, content_node)
self.druid = self:get_druid() self.druid = self:get_druid()
self.view_node = self:get_node(view_node) self.view_node = self:get_node(view_node)
@@ -186,9 +210,9 @@ function Scroll.init(self, view_node, content_node)
self.hover.on_mouse_hover:subscribe(self._on_mouse_hover) self.hover.on_mouse_hover:subscribe(self._on_mouse_hover)
self._is_mouse_hover = false self._is_mouse_hover = false
self.on_scroll = Event() self.on_scroll = event.create()
self.on_scroll_to = Event() self.on_scroll_to = event.create()
self.on_point_scroll = Event() self.on_point_scroll = event.create()
self.selected = nil self.selected = nil
self.is_animate = false self.is_animate = false
@@ -203,8 +227,8 @@ function Scroll.init(self, view_node, content_node)
end end
function Scroll.on_late_init(self) function M:on_late_init()
if not self.click_zone and const.IS_STENCIL_CHECK then if not self.click_zone then
local stencil_node = helper.get_closest_stencil_node(self.node) local stencil_node = helper.get_closest_stencil_node(self.node)
if stencil_node then if stencil_node then
self:set_click_zone(stencil_node) self:set_click_zone(stencil_node)
@@ -213,15 +237,15 @@ function Scroll.on_late_init(self)
end end
function Scroll.on_layout_change(self) function M:on_layout_change()
gui.set_position(self.content_node, self.position) gui.set_position(self.content_node, self.position)
end end
function Scroll.update(self, dt) function M:update(dt)
if self.is_animate then if self.is_animate then
self.position.x = gui.get(self.content_node, "position.x") self.position.x = gui.get(self.content_node, "position.x") --[[@as number]]
self.position.y = gui.get(self.content_node, "position.y") self.position.y = gui.get(self.content_node, "position.y") --[[@as number]]
self.on_scroll:trigger(self:get_context(), self.position) self.on_scroll:trigger(self:get_context(), self.position)
end end
@@ -233,23 +257,22 @@ function Scroll.update(self, dt)
end end
function Scroll.on_input(self, action_id, action) function M:on_input(action_id, action)
return self:_process_scroll_wheel(action_id, action) return self:_process_scroll_wheel(action_id, action)
end end
function Scroll.on_remove(self) function M:on_remove()
self:bind_grid(nil) self:bind_grid(nil)
end end
--- Start scroll to target point. --- Start scroll to target point.
-- @tparam Scroll self @{Scroll} ---@param point vector3 Target point
-- @tparam vector3 point Target point ---@param is_instant boolean|nil Instant scroll flag
-- @tparam boolean|nil is_instant Instant scroll flag
-- @usage scroll:scroll_to(vmath.vector3(0, 50, 0)) -- @usage scroll:scroll_to(vmath.vector3(0, 50, 0))
-- @usage scroll:scroll_to(vmath.vector3(0), true) -- @usage scroll:scroll_to(vmath.vector3(0), true)
function Scroll.scroll_to(self, point, is_instant) function M:scroll_to(point, is_instant)
local b = self.available_pos local b = self.available_pos
local target = vmath.vector3( local target = vmath.vector3(
self._is_horizontal_scroll and -point.x or self.target_position.x, self._is_horizontal_scroll and -point.x or self.target_position.x,
@@ -278,10 +301,9 @@ end
--- Scroll to item in scroll by point index. --- Scroll to item in scroll by point index.
-- @tparam Scroll self @{Scroll} ---@param index number Point index
-- @tparam number index Point index ---@param skip_cb boolean|nil If true, skip the point callback
-- @tparam boolean|nil skip_cb If true, skip the point callback function M:scroll_to_index(index, skip_cb)
function Scroll.scroll_to_index(self, index, skip_cb)
if not self.points then if not self.points then
return return
end end
@@ -301,11 +323,10 @@ end
--- Start scroll to target scroll percent --- Start scroll to target scroll percent
-- @tparam Scroll self @{Scroll} ---@param percent vector3 target percent
-- @tparam vector3 percent target percent ---@param is_instant boolean|nil instant scroll flag
-- @tparam boolean|nil is_instant instant scroll flag
-- @usage scroll:scroll_to_percent(vmath.vector3(0.5, 0, 0)) -- @usage scroll:scroll_to_percent(vmath.vector3(0.5, 0, 0))
function Scroll.scroll_to_percent(self, percent, is_instant) function M:scroll_to_percent(percent, is_instant)
local border = self.available_pos local border = self.available_pos
local pos = vmath.vector3( local pos = vmath.vector3(
@@ -327,9 +348,8 @@ end
--- Return current scroll progress status. --- Return current scroll progress status.
-- Values will be in [0..1] interval -- Values will be in [0..1] interval
-- @tparam Scroll self @{Scroll} ---@return vector3 New vector with scroll progress values
-- @treturn vector3 New vector with scroll progress values function M:get_percent()
function Scroll.get_percent(self)
local x_perc = 1 - inverse_lerp(self.available_pos.x, self.available_pos.z, self.position.x) local x_perc = 1 - inverse_lerp(self.available_pos.x, self.available_pos.z, self.position.x)
local y_perc = inverse_lerp(self.available_pos.w, self.available_pos.y, self.position.y) local y_perc = inverse_lerp(self.available_pos.w, self.available_pos.y, self.position.y)
@@ -339,11 +359,10 @@ end
--- Set scroll content size. --- Set scroll content size.
-- It will change content gui node size -- It will change content gui node size
-- @tparam Scroll self @{Scroll} ---@param size vector3 The new size for content node
-- @tparam vector3 size The new size for content node ---@param offset vector3|nil Offset value to set, where content is starts
-- @tparam vector3|nil offset Offset value to set, where content is starts ---@return druid.scroll Current scroll instance
-- @treturn druid.scroll Current scroll instance function M:set_size(size, offset)
function Scroll.set_size(self, size, offset)
if offset then if offset then
self._offset = offset self._offset = offset
end end
@@ -355,10 +374,9 @@ end
--- Set new scroll view size in case the node size was changed. --- Set new scroll view size in case the node size was changed.
-- @tparam Scroll self @{Scroll} ---@param size vector3 The new size for view node
-- @tparam vector3 size The new size for view node ---@return druid.scroll Current scroll instance
-- @treturn druid.scroll Current scroll instance function M:set_view_size(size)
function Scroll.set_view_size(self, size)
gui.set_size(self.view_node, size) gui.set_size(self.view_node, size)
self.view_size = size self.view_size = size
self.view_border = helper.get_border(self.view_node) self.view_border = helper.get_border(self.view_node)
@@ -369,8 +387,7 @@ end
--- Refresh scroll view size --- Refresh scroll view size
-- @tparam Scroll self @{Scroll} function M:update_view_size()
function Scroll.update_view_size(self)
self.view_size = helper.get_scaled_size(self.view_node) self.view_size = helper.get_scaled_size(self.view_node)
self.view_border = helper.get_border(self.view_node) self.view_border = helper.get_border(self.view_node)
self:_update_size() self:_update_size()
@@ -382,10 +399,9 @@ end
--- Enable or disable scroll inert. --- Enable or disable scroll inert.
-- If disabled, scroll through points (if exist) -- If disabled, scroll through points (if exist)
-- If no points, just simple drag without inertion -- If no points, just simple drag without inertion
-- @tparam Scroll self @{Scroll} ---@param state boolean Inert scroll state
-- @tparam boolean|nil state Inert scroll state ---@return druid.scroll Current scroll instance
-- @treturn druid.scroll Current scroll instance function M:set_inert(state)
function Scroll.set_inert(self, state)
self._is_inert = state self._is_inert = state
return self return self
@@ -393,19 +409,17 @@ end
--- Return if scroll have inertion. --- Return if scroll have inertion.
-- @tparam Scroll self @{Scroll} ---@return boolean @If scroll have inertion
-- @treturn boolean @If scroll have inertion function M:is_inert()
function Scroll.is_inert(self)
return self._is_inert return self._is_inert
end end
--- Set extra size for scroll stretching. --- Set extra size for scroll stretching.
-- Set 0 to disable stretching effect -- Set 0 to disable stretching effect
-- @tparam Scroll self @{Scroll} ---@param stretch_size number|nil Size in pixels of additional scroll area
-- @tparam number|nil stretch_size Size in pixels of additional scroll area ---@return druid.scroll Current scroll instance
-- @treturn druid.scroll Current scroll instance function M:set_extra_stretch_size(stretch_size)
function Scroll.set_extra_stretch_size(self, stretch_size)
self.style.EXTRA_STRETCH_SIZE = stretch_size or 0 self.style.EXTRA_STRETCH_SIZE = stretch_size or 0
self:_update_size() self:_update_size()
@@ -414,19 +428,17 @@ end
--- Return vector of scroll size with width and height. --- Return vector of scroll size with width and height.
-- @tparam Scroll self @{Scroll} ---@return vector3 Available scroll size
-- @treturn vector3 Available scroll size function M:get_scroll_size()
function Scroll.get_scroll_size(self)
return self.available_size return self.available_size
end end
--- Set points of interest. --- Set points of interest.
-- Scroll will always centered on closer points -- Scroll will always centered on closer points
-- @tparam Scroll self @{Scroll} ---@param points table Array of vector3 points
-- @tparam table points Array of vector3 points ---@return druid.scroll Current scroll instance
-- @treturn druid.scroll Current scroll instance function M:set_points(points)
function Scroll.set_points(self, points)
self.points = points self.points = points
table.sort(self.points, function(a, b) table.sort(self.points, function(a, b)
@@ -440,33 +452,30 @@ end
--- Lock or unlock horizontal scroll --- Lock or unlock horizontal scroll
-- @tparam Scroll self @{Scroll} ---@param state boolean True, if horizontal scroll is enabled
-- @tparam boolean|nil state True, if horizontal scroll is enabled ---@return druid.scroll Current scroll instance
-- @treturn druid.scroll Current scroll instance function M:set_horizontal_scroll(state)
function Scroll.set_horizontal_scroll(self, state)
self._is_horizontal_scroll = state self._is_horizontal_scroll = state
self.drag.can_x = self.available_size.x > 0 and state self.drag.can_x = self.available_size.x > 0 and state or false
return self return self
end end
--- Lock or unlock vertical scroll --- Lock or unlock vertical scroll
-- @tparam Scroll self @{Scroll} ---@param state boolean True, if vertical scroll is enabled
-- @tparam boolean|nil state True, if vertical scroll is enabled ---@return druid.scroll Current scroll instance
-- @treturn druid.scroll Current scroll instance function M:set_vertical_scroll(state)
function Scroll.set_vertical_scroll(self, state)
self._is_vertical_scroll = state self._is_vertical_scroll = state
self.drag.can_y = self.available_size.y > 0 and state self.drag.can_y = self.available_size.y > 0 and state or false
return self return self
end end
--- Check node if it visible now on scroll. --- Check node if it visible now on scroll.
-- Extra border is not affected. Return true for elements in extra scroll zone -- Extra border is not affected. Return true for elements in extra scroll zone
-- @tparam Scroll self @{Scroll} ---@param node node The node to check
-- @tparam node node The node to check ---@return boolean True if node in visible scroll area
-- @treturn boolean True if node in visible scroll area function M:is_node_in_view(node)
function Scroll.is_node_in_view(self, node)
local node_offset_for_view = gui.get_position(node) local node_offset_for_view = gui.get_position(node)
local parent = gui.get_parent(node) local parent = gui.get_parent(node)
local is_parent_of_view = false local is_parent_of_view = false
@@ -504,10 +513,9 @@ end
--- Bind the grid component (Static or Dynamic) to recalculate --- Bind the grid component (Static or Dynamic) to recalculate
-- scroll size on grid changes -- scroll size on grid changes
-- @tparam Scroll self @{Scroll} ---@param grid druid.grid|nil Druid grid component
-- @tparam StaticGrid grid Druid grid component ---@return druid.scroll Current scroll instance
-- @treturn druid.scroll Current scroll instance function M:bind_grid(grid)
function Scroll.bind_grid(self, grid)
if self._grid_on_change then if self._grid_on_change then
self._grid_on_change:unsubscribe(self._grid_on_change_callback) self._grid_on_change:unsubscribe(self._grid_on_change_callback)
@@ -516,15 +524,16 @@ function Scroll.bind_grid(self, grid)
end end
if not grid then if not grid then
return return self
end end
self._grid_on_change = grid.on_change_items self._grid_on_change = grid.on_change_items
self._grid_on_change_callback = self._grid_on_change:subscribe(function() self._grid_on_change_callback = function()
local size = grid:get_size() local size = grid:get_size()
local offset = grid:get_offset() local offset = grid:get_offset()
self:set_size(size, offset) self:set_size(size, offset)
end) end
self._grid_on_change:subscribe(self._grid_on_change_callback)
self:set_size(grid:get_size(), grid:get_offset()) self:set_size(grid:get_size(), grid:get_offset())
return self return self
@@ -533,14 +542,13 @@ end
--- Strict drag scroll area. Useful for --- Strict drag scroll area. Useful for
-- restrict events outside stencil node -- restrict events outside stencil node
-- @tparam Drag self ---@param node node|string Gui node
-- @tparam node|string node Gui node function M:set_click_zone(node)
function Scroll.set_click_zone(self, node)
self.drag:set_click_zone(node) self.drag:set_click_zone(node)
end end
function Scroll._on_scroll_drag(self, dx, dy) function M:_on_scroll_drag(dx, dy)
local t = self.target_position local t = self.target_position
local b = self.available_pos local b = self.available_pos
local eb = self.available_pos_extra local eb = self.available_pos_extra
@@ -581,7 +589,7 @@ function Scroll._on_scroll_drag(self, dx, dy)
end end
function Scroll._check_soft_zone(self) function M:_check_soft_zone()
local target = self.target_position local target = self.target_position
local border = self.available_pos local border = self.available_pos
local speed = self.style.BACK_SPEED local speed = self.style.BACK_SPEED
@@ -610,7 +618,7 @@ end
-- Cancel animation on other animation or input touch -- Cancel animation on other animation or input touch
function Scroll._cancel_animate(self) function M:_cancel_animate()
self.inertion.x = 0 self.inertion.x = 0
self.inertion.y = 0 self.inertion.y = 0
@@ -624,7 +632,7 @@ function Scroll._cancel_animate(self)
end end
function Scroll._set_scroll_position(self, position_x, position_y) function M:_set_scroll_position(position_x, position_y)
local available_extra = self.available_pos_extra local available_extra = self.available_pos_extra
position_x = helper.clamp(position_x, available_extra.x, available_extra.z) position_x = helper.clamp(position_x, available_extra.x, available_extra.z)
position_y = helper.clamp(position_y, available_extra.w, available_extra.y) position_y = helper.clamp(position_y, available_extra.w, available_extra.y)
@@ -642,8 +650,8 @@ end
--- Find closer point of interest --- Find closer point of interest
-- if no inert, scroll to next point by scroll direction -- if no inert, scroll to next point by scroll direction
-- if inert, find next point by scroll director -- if inert, find next point by scroll director
-- @local ---@private
function Scroll._check_points(self) function M:_check_points()
if not self.points then if not self.points then
return return
end end
@@ -699,7 +707,7 @@ function Scroll._check_points(self)
end end
function Scroll._check_threshold(self) function M:_check_threshold()
local is_stopped = false local is_stopped = false
if self.drag.can_x and math.abs(self.inertion.x) < self.style.INERT_THRESHOLD then if self.drag.can_x and math.abs(self.inertion.x) < self.style.INERT_THRESHOLD then
@@ -717,7 +725,7 @@ function Scroll._check_threshold(self)
end end
function Scroll._update_free_scroll(self, dt) function M:_update_free_scroll(dt)
if self.is_animate then if self.is_animate then
return return
end end
@@ -742,7 +750,7 @@ function Scroll._update_free_scroll(self, dt)
end end
function Scroll._update_hand_scroll(self, dt) function M:_update_hand_scroll(dt)
if self.is_animate then if self.is_animate then
self:_cancel_animate() self:_cancel_animate()
end end
@@ -757,7 +765,7 @@ function Scroll._update_hand_scroll(self, dt)
end end
function Scroll._on_touch_start(self) function M:_on_touch_start()
self.inertion.x = 0 self.inertion.x = 0
self.inertion.y = 0 self.inertion.y = 0
self.target_position.x = self.position.x self.target_position.x = self.position.x
@@ -765,12 +773,12 @@ function Scroll._on_touch_start(self)
end end
function Scroll._on_touch_end(self) function M:_on_touch_end()
self:_check_threshold() self:_check_threshold()
end end
function Scroll._update_size(self) function M:_update_size()
local content_border = helper.get_border(self.content_node) local content_border = helper.get_border(self.content_node)
local content_size = helper.get_scaled_size(self.content_node) local content_size = helper.get_scaled_size(self.content_node)
@@ -805,10 +813,12 @@ function Scroll._update_size(self)
self:_set_scroll_position(self.position.x, self.position.y) self:_set_scroll_position(self.position.x, self.position.y)
self.target_position.x = self.position.x self.target_position.x = self.position.x
self.target_position.y = self.position.y self.target_position.y = self.position.y
self.drag:set_drag_cursors(self.drag.can_x or self.drag.can_y)
end end
function Scroll._process_scroll_wheel(self, action_id, action) function M:_process_scroll_wheel(action_id, action)
if not self._is_mouse_hover or self.style.WHEEL_SCROLL_SPEED == 0 then if not self._is_mouse_hover or self.style.WHEEL_SCROLL_SPEED == 0 then
return false return false
end end
@@ -845,9 +855,9 @@ function Scroll._process_scroll_wheel(self, action_id, action)
end end
function Scroll._on_mouse_hover(self, state) function M:_on_mouse_hover(state)
self._is_mouse_hover = state self._is_mouse_hover = state
end end
return Scroll return M

View File

@@ -34,22 +34,22 @@
-- <a href="https://insality.github.io/druid/druid/index.html?example=general_grid" target="_blank"><b>Example Link</b></a> -- <a href="https://insality.github.io/druid/druid/index.html?example=general_grid" target="_blank"><b>Example Link</b></a>
-- @module StaticGrid -- @module StaticGrid
-- @within BaseComponent -- @within BaseComponent
-- @alias druid.static_grid -- @alias druid.grid
--- On item add callback(self, node, index) --- On item add callback(self, node, index)
-- @tfield DruidEvent on_add_item @{DruidEvent} -- @tfield event on_add_item event
--- On item remove callback(self, index) --- On item remove callback(self, index)
-- @tfield DruidEvent on_remove_item @{DruidEvent} -- @tfield event on_remove_item event
--- On item add, remove or change in_row callback(self, index|nil) --- On item add, remove or change in_row callback(self, index|nil)
-- @tfield DruidEvent on_change_items @{DruidEvent} -- @tfield event on_change_items event
--- On grid clear callback(self) --- On grid clear callback(self)
-- @tfield DruidEvent on_clear @{DruidEvent} -- @tfield event on_clear event
--- On update item positions callback(self) --- On update item positions callback(self)
-- @tfield DruidEvent on_update_positions @{DruidEvent} -- @tfield event on_update_positions event
--- Parent gui node --- Parent gui node
-- @tfield node parent -- @tfield node parent
@@ -78,11 +78,27 @@
--- ---
local const = require("druid.const") local const = require("druid.const")
local Event = require("druid.event") local event = require("event.event")
local helper = require("druid.helper") local helper = require("druid.helper")
local component = require("druid.component") local component = require("druid.component")
local StaticGrid = component.create("static_grid") ---@class druid.grid: druid.base_component
---@field on_add_item event
---@field on_remove_item event
---@field on_change_items event
---@field on_clear event
---@field on_update_positions event
---@field parent node
---@field nodes node[]
---@field first_index number
---@field last_index number
---@field anchor vector3
---@field pivot vector3
---@field node_size vector3
---@field border vector4
---@field in_row number
---@field style table
local M = component.create("static_grid")
local function _extend_border(border, pos, size, pivot) local function _extend_border(border, pos, size, pivot)
@@ -104,23 +120,22 @@ end
-- @table style -- @table style
-- @tfield boolean|nil IS_DYNAMIC_NODE_POSES If true, always center grid content as grid pivot sets. Default: false -- @tfield boolean|nil IS_DYNAMIC_NODE_POSES If true, always center grid content as grid pivot sets. Default: false
-- @tfield boolean|nil IS_ALIGN_LAST_ROW If true, always align last row of the grid as grid pivot sets. Default: false -- @tfield boolean|nil IS_ALIGN_LAST_ROW If true, always align last row of the grid as grid pivot sets. Default: false
function StaticGrid.on_style_change(self, style) function M:on_style_change(style)
self.style = {} self.style = {}
self.style.IS_DYNAMIC_NODE_POSES = style.IS_DYNAMIC_NODE_POSES or false self.style.IS_DYNAMIC_NODE_POSES = style.IS_DYNAMIC_NODE_POSES or false
self.style.IS_ALIGN_LAST_ROW = style.IS_ALIGN_LAST_ROW or false self.style.IS_ALIGN_LAST_ROW = style.IS_ALIGN_LAST_ROW or false
end end
--- The @{StaticGrid} constructor --- The StaticGrid constructor
-- @tparam StaticGrid self @{StaticGrid} ---@param parent string|node The GUI Node container, where grid's items will be placed
-- @tparam string|node parent The GUI Node container, where grid's items will be placed ---@param element node Element prefab. Need to get it size
-- @tparam node element Element prefab. Need to get it size ---@param in_row number|nil How many nodes in row can be placed. By default 1
-- @tparam number|nil in_row How many nodes in row can be placed. By default 1 function M:init(parent, element, in_row)
function StaticGrid.init(self, parent, element, in_row)
self.parent = self:get_node(parent) self.parent = self:get_node(parent)
self.nodes = {} self.nodes = {}
self.pivot = helper.get_pivot_offset(gui.get_pivot(self.parent)) self.pivot = helper.get_pivot_offset(self.parent)
self.anchor = vmath.vector3(0.5 + self.pivot.x, 0.5 - self.pivot.y, 0) self.anchor = vmath.vector3(0.5 + self.pivot.x, 0.5 - self.pivot.y, 0)
self.in_row = in_row or 1 self.in_row = in_row or 1
@@ -137,11 +152,11 @@ function StaticGrid.init(self, parent, element, in_row)
self.border = vmath.vector4(0) -- Current grid content size self.border = vmath.vector4(0) -- Current grid content size
self.on_add_item = Event() self.on_add_item = event.create()
self.on_remove_item = Event() self.on_remove_item = event.create()
self.on_change_items = Event() self.on_change_items = event.create()
self.on_clear = Event() self.on_clear = event.create()
self.on_update_positions = Event() self.on_update_positions = event.create()
self._set_position_function = gui.set_position self._set_position_function = gui.set_position
end end
@@ -149,10 +164,9 @@ end
local _temp_pos = vmath.vector3(0) local _temp_pos = vmath.vector3(0)
--- Return pos for grid node index --- Return pos for grid node index
-- @tparam StaticGrid self @{StaticGrid} ---@param index number The grid element index
-- @tparam number index The grid element index ---@return vector3 @Node position
-- @treturn vector3 @Node position function M:get_pos(index)
function StaticGrid.get_pos(self, index)
local row = math.ceil(index / self.in_row) - 1 local row = math.ceil(index / self.in_row) - 1
local col = (index - row * self.in_row) - 1 local col = (index - row * self.in_row) - 1
@@ -167,10 +181,9 @@ end
--- Return index for grid pos --- Return index for grid pos
-- @tparam StaticGrid self @{StaticGrid} ---@param pos vector3 The node position in the grid
-- @tparam vector3 pos The node position in the grid ---@return number The node index
-- @treturn number The node index function M:get_index(pos)
function StaticGrid.get_index(self, pos)
-- Offset to left-top corner from node pivot -- Offset to left-top corner from node pivot
local node_offset_x = self.node_size.x * (-0.5 + self.node_pivot.x) local node_offset_x = self.node_size.x * (-0.5 + self.node_pivot.x)
local node_offset_y = self.node_size.y * (0.5 - self.node_pivot.y) local node_offset_y = self.node_size.y * (0.5 - self.node_pivot.y)
@@ -187,10 +200,9 @@ end
--- Return grid index by node --- Return grid index by node
-- @tparam StaticGrid self @{StaticGrid} ---@param node node The gui node in the grid
-- @tparam node node The gui node in the grid ---@return number|nil index The node index
-- @treturn number The node index function M:get_index_by_node(node)
function StaticGrid.get_index_by_node(self, node)
for index, grid_node in pairs(self.nodes) do for index, grid_node in pairs(self.nodes) do
if node == grid_node then if node == grid_node then
return index return index
@@ -201,28 +213,26 @@ function StaticGrid.get_index_by_node(self, node)
end end
function StaticGrid.on_layout_change(self) function M:on_layout_change()
self:_update(true) self:_update(true)
end end
--- Set grid anchor. Default anchor is equal to anchor of grid parent node --- Set grid anchor. Default anchor is equal to anchor of grid parent node
-- @tparam StaticGrid self @{StaticGrid} ---@param anchor vector3 Anchor
-- @tparam vector3 anchor Anchor function M:set_anchor(anchor)
function StaticGrid.set_anchor(self, anchor)
self.anchor = anchor self.anchor = anchor
self:_update() self:_update()
end end
--- Update grid content --- Update grid content
-- @tparam StaticGrid self @{StaticGrid} function M:refresh()
function StaticGrid.refresh(self)
self:_update(true) self:_update(true)
end end
function StaticGrid.set_pivot(self, pivot) function M:set_pivot(pivot)
local prev_pivot = helper.get_pivot_offset(gui.get_pivot(self.parent)) local prev_pivot = helper.get_pivot_offset(gui.get_pivot(self.parent))
self.pivot = helper.get_pivot_offset(pivot) self.pivot = helper.get_pivot_offset(pivot)
@@ -254,12 +264,11 @@ end
--- Add new item to the grid --- Add new item to the grid
-- @tparam StaticGrid self @{StaticGrid} ---@param item node GUI node
-- @tparam node item GUI node ---@param index number|nil The item position. By default add as last item
-- @tparam number|nil index The item position. By default add as last item ---@param shift_policy number|nil How shift nodes, if required. Default: const.SHIFT.RIGHT
-- @tparam number|nil shift_policy How shift nodes, if required. Default: const.SHIFT.RIGHT ---@param is_instant boolean|nil If true, update node positions instantly
-- @tparam boolean|nil is_instant If true, update node positions instantly function M:add(item, index, shift_policy, is_instant)
function StaticGrid.add(self, item, index, shift_policy, is_instant)
index = index or ((self.last_index or 0) + 1) index = index or ((self.last_index or 0) + 1)
helper.insert_with_shift(self.nodes, item, index, shift_policy) helper.insert_with_shift(self.nodes, item, index, shift_policy)
@@ -279,10 +288,9 @@ end
--- Set new items to the grid. All previous items will be removed --- Set new items to the grid. All previous items will be removed
-- @tparam StaticGrid self @{StaticGrid} ---@param nodes node[] The new grid nodes
-- @tparam node[] nodes The new grid nodes
-- @tparam[opt=false] boolean is_instant If true, update node positions instantly -- @tparam[opt=false] boolean is_instant If true, update node positions instantly
function StaticGrid.set_items(self, nodes, is_instant) function M:set_items(nodes, is_instant)
self.nodes = nodes self.nodes = nodes
for index = 1, #nodes do for index = 1, #nodes do
local item = nodes[index] local item = nodes[index]
@@ -296,12 +304,11 @@ end
--- Remove the item from the grid. Note that gui node will be not deleted --- Remove the item from the grid. Note that gui node will be not deleted
-- @tparam StaticGrid self @{StaticGrid} ---@param index number The grid node index to remove
-- @tparam number index The grid node index to remove ---@param shift_policy number|nil How shift nodes, if required. Default: const.SHIFT.RIGHT
-- @tparam number|nil shift_policy How shift nodes, if required. Default: const.SHIFT.RIGHT ---@param is_instant boolean|nil If true, update node positions instantly
-- @tparam boolean|nil is_instant If true, update node positions instantly ---@return node The deleted gui node from grid
-- @treturn node The deleted gui node from grid function M:remove(index, shift_policy, is_instant)
function StaticGrid.remove(self, index, shift_policy, is_instant)
assert(self.nodes[index], "No grid item at given index " .. index) assert(self.nodes[index], "No grid item at given index " .. index)
local remove_node = self.nodes[index] local remove_node = self.nodes[index]
@@ -317,9 +324,8 @@ end
--- Return grid content size --- Return grid content size
-- @tparam StaticGrid self @{StaticGrid} ---@return vector3 The grid content size
-- @treturn vector3 The grid content size function M:get_size()
function StaticGrid.get_size(self)
return vmath.vector3( return vmath.vector3(
self.border.z - self.border.x, self.border.z - self.border.x,
self.border.y - self.border.w, self.border.y - self.border.w,
@@ -327,7 +333,7 @@ function StaticGrid.get_size(self)
end end
function StaticGrid.get_size_for(self, count) function M:get_size_for(count)
if not count or count == 0 then if not count or count == 0 then
return vmath.vector3(0) return vmath.vector3(0)
end end
@@ -350,17 +356,15 @@ end
--- Return grid content borders --- Return grid content borders
-- @tparam StaticGrid self @{StaticGrid} ---@return vector4 The grid content borders
-- @treturn vector3 The grid content borders function M:get_borders()
function StaticGrid.get_borders(self)
return self.border return self.border
end end
--- Return array of all node positions --- Return array of all node positions
-- @tparam StaticGrid self @{StaticGrid} ---@return vector3[] All grid node positions
-- @treturn vector3[] All grid node positions function M:get_all_pos()
function StaticGrid.get_all_pos(self)
local result = {} local result = {}
for i, node in pairs(self.nodes) do for i, node in pairs(self.nodes) do
table.insert(result, gui.get_position(node)) table.insert(result, gui.get_position(node))
@@ -372,10 +376,9 @@ end
--- Change set position function for grid nodes. It will call on --- Change set position function for grid nodes. It will call on
-- update poses on grid elements. Default: gui.set_position -- update poses on grid elements. Default: gui.set_position
-- @tparam StaticGrid self @{StaticGrid} ---@param callback function Function on node set position
-- @tparam function callback Function on node set position ---@return druid.grid Current grid instance
-- @treturn druid.static_grid Current grid instance function M:set_position_function(callback)
function StaticGrid.set_position_function(self, callback)
self._set_position_function = callback or gui.set_position self._set_position_function = callback or gui.set_position
return self return self
@@ -384,9 +387,8 @@ end
--- Clear grid nodes array. GUI nodes will be not deleted! --- Clear grid nodes array. GUI nodes will be not deleted!
-- If you want to delete GUI nodes, use static_grid.nodes array before grid:clear -- If you want to delete GUI nodes, use static_grid.nodes array before grid:clear
-- @tparam StaticGrid self @{StaticGrid} ---@return druid.grid Current grid instance
-- @treturn druid.static_grid Current grid instance function M:clear()
function StaticGrid.clear(self)
self.border.x = 0 self.border.x = 0
self.border.y = 0 self.border.y = 0
self.border.w = 0 self.border.w = 0
@@ -403,9 +405,8 @@ end
--- Return StaticGrid offset, where StaticGrid content starts. --- Return StaticGrid offset, where StaticGrid content starts.
-- @tparam StaticGrid self @{StaticGrid} The StaticGrid instance ---@return vector3 The StaticGrid offset
-- @treturn vector3 The StaticGrid offset function M:get_offset()
function StaticGrid:get_offset()
local borders = self:get_borders() local borders = self:get_borders()
local size = self:get_size() local size = self:get_size()
@@ -419,10 +420,9 @@ end
--- Set new in_row elements for grid --- Set new in_row elements for grid
-- @tparam StaticGrid self @{StaticGrid} ---@param in_row number The new in_row value
-- @tparam number in_row The new in_row value ---@return druid.grid Current grid instance
-- @treturn druid.static_grid Current grid instance function M:set_in_row(in_row)
function StaticGrid.set_in_row(self, in_row)
self.in_row = in_row self.in_row = in_row
self._grid_horizonal_offset = self.node_size.x * (self.in_row - 1) * self.anchor.x self._grid_horizonal_offset = self.node_size.x * (self.in_row - 1) * self.anchor.x
self._zero_offset = vmath.vector3( self._zero_offset = vmath.vector3(
@@ -438,11 +438,10 @@ end
--- Set new node size for grid --- Set new node size for grid
-- @tparam StaticGrid self @{StaticGrid}
-- @tparam[opt] number width The new node width -- @tparam[opt] number width The new node width
-- @tparam[opt] number height The new node height -- @tparam[opt] number height The new node height
-- @treturn druid.static_grid Current grid instance ---@return druid.grid Current grid instance
function StaticGrid.set_item_size(self, width, height) function M:set_item_size(width, height)
if width then if width then
self.node_size.x = width self.node_size.x = width
end end
@@ -463,20 +462,20 @@ end
--- Sort grid nodes by custom comparator function --- Sort grid nodes by custom comparator function
-- @tparam StaticGrid self @{StaticGrid} ---@param comparator function The comparator function. (a, b) -> boolean
-- @tparam function comparator The comparator function. (a, b) -> boolean ---@return druid.grid self Current grid instance
-- @treturn druid.static_grid Current grid instance function M:sort_nodes(comparator)
function StaticGrid.sort_nodes(self, comparator)
table.sort(self.nodes, comparator) table.sort(self.nodes, comparator)
self:_update(true) self:_update(true)
return self
end end
--- Update grid inner state --- Update grid inner state
-- @tparam StaticGrid self @{StaticGrid} ---@param is_instant boolean|nil If true, node position update instantly, otherwise with set_position_function callback
-- @tparam boolean|nil is_instant If true, node position update instantly, otherwise with set_position_function callback ---@private
-- @local function M:_update(is_instant)
function StaticGrid._update(self, is_instant)
self:_update_indexes() self:_update_indexes()
self:_update_borders() self:_update_borders()
self:_update_pos(is_instant) self:_update_pos(is_instant)
@@ -484,9 +483,8 @@ end
--- Update first and last indexes of grid nodes --- Update first and last indexes of grid nodes
-- @tparam StaticGrid self @{StaticGrid} ---@private
-- @local function M:_update_indexes()
function StaticGrid._update_indexes(self)
self.first_index = nil self.first_index = nil
self.last_index = nil self.last_index = nil
for index in pairs(self.nodes) do for index in pairs(self.nodes) do
@@ -500,9 +498,8 @@ end
--- Update grid content borders, recalculate min and max values --- Update grid content borders, recalculate min and max values
-- @tparam StaticGrid self @{StaticGrid} ---@private
-- @local function M:_update_borders()
function StaticGrid._update_borders(self)
if not self.first_index then if not self.first_index then
self.border = vmath.vector4(0) self.border = vmath.vector4(0)
return return
@@ -519,10 +516,9 @@ end
--- Update grid nodes position --- Update grid nodes position
-- @tparam StaticGrid self @{StaticGrid} ---@param is_instant boolean|nil If true, node position update instantly, otherwise with set_position_function callback
-- @tparam boolean|nil is_instant If true, node position update instantly, otherwise with set_position_function callback ---@private
-- @local function M:_update_pos(is_instant)
function StaticGrid._update_pos(self, is_instant)
local zero_offset = self:_get_zero_offset() local zero_offset = self:_get_zero_offset()
for i, node in pairs(self.nodes) do for i, node in pairs(self.nodes) do
@@ -543,11 +539,11 @@ end
--- Return elements offset for correct posing nodes. Correct posing at --- Return elements offset for correct posing nodes. Correct posing at
-- parent pivot node (0:0) with adjusting of node sizes and anchoring -- parent pivot node (0:0) with adjusting of node sizes and anchoring
-- @treturn vector3 The offset vector ---@return vector3 The offset vector
-- @local ---@private
function StaticGrid:_get_zero_offset() function M:_get_zero_offset()
if not self.style.IS_DYNAMIC_NODE_POSES then if not self.style.IS_DYNAMIC_NODE_POSES then
return const.VECTOR_ZERO return vmath.vector3(0)
end end
-- zero offset: center pos - border size * anchor -- zero offset: center pos - border size * anchor
@@ -560,9 +556,9 @@ end
--- Return offset x for last row in grid. Used to align this row accorting to grid's anchor --- Return offset x for last row in grid. Used to align this row accorting to grid's anchor
-- @treturn number The offset x value ---@return number The offset x value
-- @local ---@private
function StaticGrid:_get_zero_offset_x(row_index) function M:_get_zero_offset_x(row_index)
if not self.style.IS_DYNAMIC_NODE_POSES or not self.style.IS_ALIGN_LAST_ROW then if not self.style.IS_DYNAMIC_NODE_POSES or not self.style.IS_ALIGN_LAST_ROW then
return self._zero_offset.x return self._zero_offset.x
end end
@@ -580,4 +576,4 @@ function StaticGrid:_get_zero_offset_x(row_index)
end end
return StaticGrid return M

View File

@@ -36,13 +36,13 @@
-- @alias druid.text -- @alias druid.text
--- On set text callback(self, text) --- On set text callback(self, text)
-- @tfield DruidEvent on_set_text @{DruidEvent} -- @tfield event on_set_text event
--- On adjust text size callback(self, new_scale, text_metrics) --- On adjust text size callback(self, new_scale, text_metrics)
-- @tfield DruidEvent on_update_text_scale @{DruidEvent} -- @tfield event on_update_text_scale event
--- On change pivot callback(self, pivot) --- On change pivot callback(self, pivot)
-- @tfield DruidEvent on_set_pivot @{DruidEvent} -- @tfield event on_set_pivot event
--- Text node --- Text node
-- @tfield node node -- @tfield node node
@@ -76,14 +76,23 @@
--- ---
local Event = require("druid.event") local event = require("event.event")
local const = require("druid.const") local const = require("druid.const")
local helper = require("druid.helper") local helper = require("druid.helper")
local utf8_lua = require("druid.system.utf8") local utf8_lua = require("druid.system.utf8")
local component = require("druid.component") local component = require("druid.component")
local utf8 = utf8 or utf8_lua --[[@as utf8]] local utf8 = utf8 or utf8_lua --[[@as utf8]]
local Text = component.create("text") ---@class druid.text: druid.base_component
---@field node node
---@field on_set_text event
---@field on_update_text_scale event
---@field on_set_pivot event
---@field style table
---@field private start_pivot userdata
---@field private start_scale vector3
---@field private scale vector3
local M = component.create("text")
local function update_text_size(self) local function update_text_size(self)
if self.scale.x == 0 or self.scale.y == 0 then if self.scale.x == 0 or self.scale.y == 0 then
@@ -200,6 +209,8 @@ local function update_text_area_size(self)
end end
---@param self druid.text
---@param trim_postfix string
local function update_text_with_trim(self, trim_postfix) local function update_text_with_trim(self, trim_postfix)
local max_width = self.text_area.x local max_width = self.text_area.x
local text_width = self:get_text_size() local text_width = self:get_text_size()
@@ -222,6 +233,24 @@ local function update_text_with_trim(self, trim_postfix)
end end
end end
local function update_text_with_trim_left(self, trim_postfix)
local max_width = self.text_area.x
local text_width = self:get_text_size()
local text_length = utf8.len(self.last_value)
local trim_index = 1
if text_width > max_width then
local new_text = self.last_value
while text_width > max_width and trim_index < text_length do
trim_index = trim_index + 1
new_text = trim_postfix .. utf8.sub(self.last_value, trim_index, text_length)
text_width = self:get_text_size(new_text)
end
gui.set_text(self.node, new_text)
end
end
local function update_text_with_anchor_shift(self) local function update_text_with_anchor_shift(self)
if self:get_text_size() >= self.text_area.x then if self:get_text_size() >= self.text_area.x then
@@ -232,6 +261,7 @@ local function update_text_with_anchor_shift(self)
end end
---@param self druid.text
local function update_adjust(self) local function update_adjust(self)
if not self.adjust_type or self.adjust_type == const.TEXT_ADJUST.NO_ADJUST then if not self.adjust_type or self.adjust_type == const.TEXT_ADJUST.NO_ADJUST then
reset_default_scale(self) reset_default_scale(self)
@@ -246,6 +276,10 @@ local function update_adjust(self)
update_text_with_trim(self, self.style.TRIM_POSTFIX) update_text_with_trim(self, self.style.TRIM_POSTFIX)
end end
if self.adjust_type == const.TEXT_ADJUST.TRIM_LEFT then
update_text_with_trim_left(self, self.style.TRIM_POSTFIX)
end
if self.adjust_type == const.TEXT_ADJUST.DOWNSCALE_LIMITED then if self.adjust_type == const.TEXT_ADJUST.DOWNSCALE_LIMITED then
update_text_area_size(self) update_text_area_size(self)
end end
@@ -258,6 +292,16 @@ local function update_adjust(self)
update_text_area_size(self) update_text_area_size(self)
update_text_with_anchor_shift(self) update_text_with_anchor_shift(self)
end end
if self.adjust_type == const.TEXT_ADJUST.SCALE_THEN_TRIM then
update_text_area_size(self)
update_text_with_trim(self, self.style.TRIM_POSTFIX)
end
if self.adjust_type == const.TEXT_ADJUST.SCALE_THEN_TRIM_LEFT then
update_text_area_size(self)
update_text_with_trim_left(self, self.style.TRIM_POSTFIX)
end
end end
@@ -269,7 +313,7 @@ end
-- @tfield string|nil DEFAULT_ADJUST The default adjust type for any text component. Default: DOWNSCALE -- @tfield string|nil DEFAULT_ADJUST The default adjust type for any text component. Default: DOWNSCALE
-- @tfield string|nil ADJUST_STEPS Amount of iterations for text adjust by height. Default: 20 -- @tfield string|nil ADJUST_STEPS Amount of iterations for text adjust by height. Default: 20
-- @tfield string|nil ADJUST_SCALE_DELTA Scale step on each height adjust step. Default: 0.02 -- @tfield string|nil ADJUST_SCALE_DELTA Scale step on each height adjust step. Default: 0.02
function Text.on_style_change(self, style) function M:on_style_change(style)
self.style = {} self.style = {}
self.style.TRIM_POSTFIX = style.TRIM_POSTFIX or "..." self.style.TRIM_POSTFIX = style.TRIM_POSTFIX or "..."
self.style.DEFAULT_ADJUST = style.DEFAULT_ADJUST or const.TEXT_ADJUST.DOWNSCALE self.style.DEFAULT_ADJUST = style.DEFAULT_ADJUST or const.TEXT_ADJUST.DOWNSCALE
@@ -278,12 +322,11 @@ function Text.on_style_change(self, style)
end end
--- The @{Text} constructor --- The Text constructor
-- @tparam Text self @{Text} ---@param node string|node Node name or GUI Text Node itself
-- @tparam string|node node Node name or GUI Text Node itself ---@param value string|nil Initial text. Default value is node text from GUI scene. Default: nil
-- @tparam string|nil value Initial text. Default value is node text from GUI scene. Default: nil ---@param adjust_type string|nil Adjust type for text. By default is DOWNSCALE. Look const.TEXT_ADJUST for reference. Default: DOWNSCALE
-- @tparam string|nil adjust_type Adjust type for text. By default is DOWNSCALE. Look const.TEXT_ADJUST for reference. Default: DOWNSCALE function M:init(node, value, adjust_type)
function Text.init(self, node, value, adjust_type)
self.node = self:get_node(node) self.node = self:get_node(node)
self.pos = gui.get_position(self.node) self.pos = gui.get_position(self.node)
self.node_id = gui.get_id(self.node) self.node_id = gui.get_id(self.node)
@@ -300,37 +343,25 @@ function Text.init(self, node, value, adjust_type)
self.adjust_type = adjust_type or self.style.DEFAULT_ADJUST self.adjust_type = adjust_type or self.style.DEFAULT_ADJUST
self.color = gui.get_color(self.node) self.color = gui.get_color(self.node)
self.on_set_text = Event() self.on_set_text = event.create()
self.on_set_pivot = Event() self.on_set_pivot = event.create()
self.on_update_text_scale = Event() self.on_update_text_scale = event.create()
self:set_to(value or gui.get_text(self.node)) self:set_text(value or gui.get_text(self.node))
return self return self
end end
function Text.on_layout_change(self) function M:on_layout_change()
self:set_to(self.last_value) self:set_text(self.last_value)
end
function Text.on_message_input(self, node_id, message)
if node_id ~= self.node_id then
return false
end
if message.action == const.MESSAGE_INPUT.TEXT_SET then
Text.set_to(self, message.value)
end
end end
--- Calculate text width with font with respect to trailing space --- Calculate text width with font with respect to trailing space
-- @tparam Text self @{Text} ---@param text string|nil
-- @tparam string text|nil ---@return number Width
-- @treturn number Width ---@return number Height
-- @treturn number Height function M:get_text_size(text)
function Text.get_text_size(self, text)
text = text or self.last_value text = text or self.last_value
local font_name = gui.get_font(self.node) local font_name = gui.get_font(self.node)
local font = gui.get_font_resource(font_name) local font = gui.get_font_resource(font_name)
@@ -351,10 +382,9 @@ end
--- Get chars count by width --- Get chars count by width
-- @tparam Text self @{Text} ---@param width number
-- @tparam number width ---@return number Chars count
-- @treturn number Chars count function M:get_text_index_by_width(width)
function Text.get_text_index_by_width(self, width)
local text = self.last_value local text = self.last_value
local font_name = gui.get_font(self.node) local font_name = gui.get_font(self.node)
local font = gui.get_font_resource(font_name) local font = gui.get_font_resource(font_name)
@@ -385,11 +415,11 @@ end
--- Set text to text field --- Set text to text field
-- @tparam Text self @{Text} ---@deprecated
-- @tparam string set_to Text for node ---@param set_to string Text for node
-- @treturn Text Current text instance ---@return druid.text Current text instance
function Text.set_to(self, set_to) function M:set_to(set_to)
set_to = set_to or "" set_to = tostring(set_to or "")
self.last_value = set_to self.last_value = set_to
gui.set_text(self.node, set_to) gui.set_text(self.node, set_to)
@@ -402,24 +432,35 @@ function Text.set_to(self, set_to)
end end
function M:set_text(new_text)
---@diagnostic disable-next-line: deprecated
return self:set_to(new_text)
end
function M:get_text()
return self.last_value
end
--- Set text area size --- Set text area size
-- @tparam Text self @{Text} ---@param size vector3 The new text area size
-- @tparam vector3 size The new text area size ---@return druid.text self Current text instance
-- @treturn Text Current text instance function M:set_size(size)
function Text.set_size(self, size)
self.start_size = size self.start_size = size
self.text_area = vmath.vector3(size) self.text_area = vmath.vector3(size)
self.text_area.x = self.text_area.x * self.start_scale.x self.text_area.x = self.text_area.x * self.start_scale.x
self.text_area.y = self.text_area.y * self.start_scale.y self.text_area.y = self.text_area.y * self.start_scale.y
update_adjust(self) update_adjust(self)
return self
end end
--- Set color --- Set color
-- @tparam Text self @{Text} ---@param color vector4 Color for node
-- @tparam vector4 color Color for node ---@return druid.text Current text instance
-- @treturn Text Current text instance function M:set_color(color)
function Text.set_color(self, color)
self.color = color self.color = color
gui.set_color(self.node, color) gui.set_color(self.node, color)
@@ -428,10 +469,9 @@ end
--- Set alpha --- Set alpha
-- @tparam Text self @{Text} ---@param alpha number Alpha for node
-- @tparam number alpha Alpha for node ---@return druid.text Current text instance
-- @treturn Text Current text instance function M:set_alpha(alpha)
function Text.set_alpha(self, alpha)
self.color.w = alpha self.color.w = alpha
gui.set_color(self.node, self.color) gui.set_color(self.node, self.color)
@@ -440,10 +480,9 @@ end
--- Set scale --- Set scale
-- @tparam Text self @{Text} ---@param scale vector3 Scale for node
-- @tparam vector3 scale Scale for node ---@return druid.text Current text instance
-- @treturn Text Current text instance function M:set_scale(scale)
function Text.set_scale(self, scale)
self.last_scale = scale self.last_scale = scale
gui.set_scale(self.node, scale) gui.set_scale(self.node, scale)
@@ -452,10 +491,9 @@ end
--- Set text pivot. Text will re-anchor inside text area --- Set text pivot. Text will re-anchor inside text area
-- @tparam Text self @{Text} ---@param pivot userdata The gui.PIVOT_* constant
-- @tparam number pivot The gui.PIVOT_* constant ---@return druid.text Current text instance
-- @treturn Text Current text instance function M:set_pivot(pivot)
function Text.set_pivot(self, pivot)
local prev_pivot = gui.get_pivot(self.node) local prev_pivot = gui.get_pivot(self.node)
local prev_offset = const.PIVOTS[prev_pivot] local prev_offset = const.PIVOTS[prev_pivot]
@@ -477,33 +515,32 @@ function Text.set_pivot(self, pivot)
end end
--- Return true, if text with line break ---Return true, if text with line break
-- @tparam Text self @{Text} ---@return boolean Is text node with line break
-- @treturn boolean Is text node with line break function M:is_multiline()
function Text.is_multiline(self)
return gui.get_line_break(self.node) return gui.get_line_break(self.node)
end end
--- Set text adjust, refresh the current text visuals, if needed ---Set text adjust, refresh the current text visuals, if needed
-- @tparam Text self @{Text} ---Values are: "downscale", "trim", "no_adjust", "downscale_limited",
-- @tparam string|nil adjust_type See const.TEXT_ADJUST. If pass nil - use current adjust type ---"scroll", "scale_then_scroll", "trim_left", "scale_then_trim", "scale_then_trim_left"
-- @tparam number|nil minimal_scale If pass nil - not use minimal scale ---@param adjust_type string|nil See const.TEXT_ADJUST. If pass nil - use current adjust type
-- @treturn Text Current text instance ---@param minimal_scale number|nil To remove minimal scale, use `text:set_minimal_scale(nil)`, if pass nil - not change minimal scale
function Text.set_text_adjust(self, adjust_type, minimal_scale) ---@return druid.text self Current text instance
function M:set_text_adjust(adjust_type, minimal_scale)
self.adjust_type = adjust_type self.adjust_type = adjust_type
self._minimal_scale = minimal_scale self._minimal_scale = minimal_scale or self._minimal_scale
self:set_to(self.last_value) self:set_text(self.last_value)
return self return self
end end
--- Set minimal scale for DOWNSCALE_LIMITED or SCALE_THEN_SCROLL adjust types --- Set minimal scale for DOWNSCALE_LIMITED or SCALE_THEN_SCROLL adjust types
-- @tparam Text self @{Text} ---@param minimal_scale number If pass nil - not use minimal scale
-- @tparam number minimal_scale If pass nil - not use minimal scale ---@return druid.text Current text instance
-- @treturn Text Current text instance function M:set_minimal_scale(minimal_scale)
function Text.set_minimal_scale(self, minimal_scale)
self._minimal_scale = minimal_scale self._minimal_scale = minimal_scale
return self return self
@@ -511,10 +548,10 @@ end
--- Return current text adjust type --- Return current text adjust type
-- @treturn number The current text adjust type ---@return string adjust_type The current text adjust type
function Text.get_text_adjust(self, adjust_type) function M:get_text_adjust()
return self.adjust_type return self.adjust_type
end end
return Text return M

48
druid/bindings.lua Normal file
View File

@@ -0,0 +1,48 @@
local event = require("event.event")
local M = {}
local WRAPPED_WIDGETS = {}
---Set a widget to the current game object. The game object can acquire the widget by calling `bindings.get_widget`
---It wraps with events only top level functions cross-context, so no access to nested widgets functions
---@param widget druid.widget
function M.set_widget(widget)
local object = msg.url()
object.fragment = nil
-- Make a copy of the widget with all functions wrapped in events
-- It makes available to call gui functions from game objects
local wrapped_widget = setmetatable({}, { __index = widget })
local parent_table = getmetatable(widget).__index
-- Go through all functions and wrap them in events
for key, value in pairs(parent_table) do
if type(value) == "function" then
wrapped_widget[key] = event.create(function(_, ...)
return value(widget, ...)
end)
end
end
WRAPPED_WIDGETS[object.socket] = WRAPPED_WIDGETS[object.socket] or {}
WRAPPED_WIDGETS[object.socket][object.path] = wrapped_widget
end
---@param object_url string|userdata|url @root object
---@return druid.widget|nil
function M.get_widget(object_url)
assert(object_url, "You must provide an object_url")
object_url = msg.url(object_url --[[@as string]])
local socket_widgets = WRAPPED_WIDGETS[object_url.socket]
if not socket_widgets then
return nil
end
return socket_widgets[object_url.path]
end
return M

229
druid/color.lua Normal file
View File

@@ -0,0 +1,229 @@
---@type table<string, table<string, vector4>>
local PALETTE_DATA
local CURRENT_PALETTE = "default"
local DEFAULT_COLOR = vmath.vector4(1, 1, 1, 1)
local COLOR_X = hash("color.x")
local COLOR_Y = hash("color.y")
local COLOR_Z = hash("color.z")
local M = {}
---Get color color by id
---@param color_id string
---@return vector4
function M.get(color_id)
-- Check is it hex: starts with "#" or contains only 3 or 6 hex symbols
if type(color_id) == "string" then
if string.sub(color_id, 1, 1) == "#" or string.match(color_id, "^[0-9a-fA-F]+$") then
return M.hex2vector4(color_id)
end
end
return PALETTE_DATA[CURRENT_PALETTE] and PALETTE_DATA[CURRENT_PALETTE][color_id] or DEFAULT_COLOR
end
---Add palette to palette data
---@param palette_name string
---@param palette_data table<string, vector4>
function M.add_palette(palette_name, palette_data)
PALETTE_DATA[palette_name] = PALETTE_DATA[palette_name] or {}
local palette = PALETTE_DATA[palette_name]
for color_id, color in pairs(palette_data) do
if type(color) == "string" then
palette[color_id] = M.hex2vector4(color)
else
palette[color_id] = color
end
end
end
function M.set_palette(palette_name)
if PALETTE_DATA[palette_name] then
CURRENT_PALETTE = palette_name
end
end
function M.get_palette()
return CURRENT_PALETTE
end
---Set color of gui node without changing alpha
---@param gui_node node
---@param color vector4|vector3|string Color in vector4, vector3 or color id from palette
function M.set_color(gui_node, color)
if type(color) == "string" then
color = M.get(color)
end
gui.set(gui_node, COLOR_X, color.x)
gui.set(gui_node, COLOR_Y, color.y)
gui.set(gui_node, COLOR_Z, color.z)
end
function M.get_random_color()
return vmath.vector4(math.random(), math.random(), math.random(), 1)
end
---Lerp colors via color HSB values
function M.lerp(t, color1, color2)
local h1, s1, v1 = M.rgb2hsb(color1.x, color1.y, color1.z)
local h2, s2, v2 = M.rgb2hsb(color2.x, color2.y, color2.z)
local h = h1 + (h2 - h1) * t
local s = s1 + (s2 - s1) * t
local v = v1 + (v2 - v1) * t
local r, g, b, a = M.hsb2rgb(h, s, v)
a = a or 1
return vmath.vector4(r, g, b, a)
end
---@param hex string
---@param alpha number|nil
---@return number, number, number, number
function M.hex2rgb(hex, alpha)
alpha = alpha or 1
if alpha > 1 then
alpha = alpha / 100
end
-- Remove leading #
if string.sub(hex, 1, 1) == "#" then
hex = string.sub(hex, 2)
end
-- Expand 3-digit hex codes to 6 digits
if #hex == 3 then
hex = string.rep(string.sub(hex, 1, 1), 2) ..
string.rep(string.sub(hex, 2, 2), 2) ..
string.rep(string.sub(hex, 3, 3), 2)
end
local r = tonumber("0x" .. string.sub(hex, 1, 2)) / 255
local g = tonumber("0x" .. string.sub(hex, 3, 4)) / 255
local b = tonumber("0x" .. string.sub(hex, 5, 6)) / 255
return r, g, b, alpha
end
---@param hex string
---@param alpha number|nil
---@return vector4
function M.hex2vector4(hex, alpha)
local r, g, b, a = M.hex2rgb(hex, alpha)
return vmath.vector4(r, g, b, a)
end
---Convert hsb color to rgb color
---@param r number @Red value
---@param g number @Green value
---@param b number @Blue value
---@param alpha number|nil @Alpha value. Default is 1
function M.rgb2hsb(r, g, b, alpha)
alpha = alpha or 1
local min, max = math.min(r, g, b), math.max(r, g, b)
local delta = max - min
local h, s, v = 0, max, max
s = max ~= 0 and delta / max or 0
if delta ~= 0 then
if r == max then
h = (g - b) / delta
elseif g == max then
h = 2 + (b - r) / delta
else
h = 4 + (r - g) / delta
end
h = (h / 6) % 1
end
alpha = alpha > 1 and alpha / 100 or alpha
return h, s, v, alpha
end
---Convert hsb color to rgb color
---@param h number @Hue
---@param s number @Saturation
---@param v number @Value
---@param alpha number|nil @Alpha value. Default is 1
function M.hsb2rgb(h, s, v, alpha)
local r, g, b
local i = math.floor(h * 6)
local f = h * 6 - i
local p = v * (1 - s)
local q = v * (1 - f * s)
local t = v * (1 - (1 - f) * s)
i = i % 6
if i == 0 then r, g, b = v, t, p
elseif i == 1 then r, g, b = q, v, p
elseif i == 2 then r, g, b = p, v, t
elseif i == 3 then r, g, b = p, q, v
elseif i == 4 then r, g, b = t, p, v
elseif i == 5 then r, g, b = v, p, q
end
return r, g, b, alpha
end
---Convert rgb color to hex color
---@param red number @Red value
---@param green number @Green value
---@param blue number @Blue value
function M.rgb2hex(red, green, blue)
local r = string.format("%x", math.floor(red * 255))
local g = string.format("%x", math.floor(green * 255))
local b = string.format("%x", math.floor(blue * 255))
return string.upper((#r == 1 and "0" or "") .. r .. (#g == 1 and "0" or "") .. g .. (#b == 1 and "0" or "") .. b)
end
function M.load_palette()
local PALETTE_PATH = sys.get_config_string("fluid.palette")
if PALETTE_PATH then
PALETTE_DATA = M.load_json(PALETTE_PATH) --[[@as table<string, table<string, vector4>>]]
end
PALETTE_DATA = PALETTE_DATA or {}
for _, palette_data in pairs(PALETTE_DATA) do
for color_id, color in pairs(palette_data) do
if type(color) == "string" then
palette_data[color_id] = M.hex2vector4(color)
end
end
end
end
---Load JSON file from game resources folder (by relative path to game.project)
---Return nil if file not found or error
---@param json_path string
---@return table|nil
function M.load_json(json_path)
local resource, is_error = sys.load_resource(json_path)
if is_error or not resource then
return nil
end
return json.decode(resource)
end
M.load_palette()
return M

View File

@@ -1,83 +1,61 @@
-- Copyright (c) 2021 Maksim Tuprikov <insality@gmail.com>. This code is licensed under MIT license
--- Basic class for all Druid components.
-- To create you custom component, use static function `component.create`
-- @usage
-- -- Create your component:
-- local component = require("druid.component")
--
-- local AwesomeComponent = component.create("awesome_component")
--
-- function AwesomeComponent:init(template, nodes)
-- self:set_template(template)
-- self:set_nodes(nodes)
-- self.druid = self:get_druid()
-- end
--
-- return AwesomeComponent
-- @module BaseComponent
-- @alias druid.base_component
local const = require("druid.const") local const = require("druid.const")
local helper = require("druid.helper") local helper = require("druid.helper")
local BaseComponent = {} ---@class druid.base_component.meta
---@field template string
---@field context table
---@field nodes table<hash, node>|nil
---@field style table|nil
---@field druid druid_instance
---@field input_enabled boolean
---@field children table
---@field parent druid.base_component|nil
---@field instance_class table
---@class druid.base_component.component
---@field name string
---@field input_priority number
---@field default_input_priority number
---@field _is_input_priority_changed boolean
---@field _uid number
---@class druid.base_component
---@field druid druid_instance Druid instance to create inner components
---@field init fun(self:druid.base_component, ...)|nil
---@field update fun(self:druid.base_component, dt:number)|nil
---@field on_remove fun(self:druid.base_component)|nil
---@field on_input fun(self:druid.base_component, action_id:number, action:table)|nil
---@field on_message fun(self:druid.base_component, message_id:hash, message:table, sender:url)|nil
---@field on_late_init fun(self:druid.base_component)|nil
---@field on_focus_lost fun(self:druid.base_component)|nil
---@field on_focus_gained fun(self:druid.base_component)|nil
---@field on_style_change fun(self:druid.base_component, style: table)|nil
---@field on_layout_change fun(self:druid.base_component)|nil
---@field on_window_resized fun(self:druid.base_component)|nil
---@field on_language_change fun(self:druid.base_component)|nil
---@field private _component druid.base_component.component
---@field private _meta druid.base_component.meta
local M = {}
local INTERESTS = {} -- Cache interests per component class in runtime local INTERESTS = {} -- Cache interests per component class in runtime
local IS_AUTO_TEMPLATE = not (sys.get_config_int("druid.no_auto_template", 0) == 1)
-- Component Interests
BaseComponent.ON_INPUT = const.ON_INPUT
BaseComponent.ON_UPDATE = const.ON_UPDATE
BaseComponent.ON_MESSAGE = const.ON_MESSAGE
BaseComponent.ON_LATE_INIT = const.ON_LATE_INIT
BaseComponent.ON_FOCUS_LOST = const.ON_FOCUS_LOST
BaseComponent.ON_FOCUS_GAINED = const.ON_FOCUS_GAINED
BaseComponent.ON_LAYOUT_CHANGE = const.ON_LAYOUT_CHANGE
BaseComponent.ON_MESSAGE_INPUT = const.ON_MESSAGE_INPUT
BaseComponent.ON_WINDOW_RESIZED = const.ON_WINDOW_RESIZED
BaseComponent.ON_LANGUAGE_CHANGE = const.ON_LANGUAGE_CHANGE
BaseComponent.ALL_INTERESTS = {
BaseComponent.ON_INPUT,
BaseComponent.ON_UPDATE,
BaseComponent.ON_MESSAGE,
BaseComponent.ON_LATE_INIT,
BaseComponent.ON_FOCUS_LOST,
BaseComponent.ON_FOCUS_GAINED,
BaseComponent.ON_LAYOUT_CHANGE,
BaseComponent.ON_MESSAGE_INPUT,
BaseComponent.ON_WINDOW_RESIZED,
BaseComponent.ON_LANGUAGE_CHANGE,
}
-- Mapping from on_message method to specific method name
BaseComponent.SPECIFIC_UI_MESSAGES = {
[hash("layout_changed")] = BaseComponent.ON_LAYOUT_CHANGE, -- The message_id from Defold
[hash(BaseComponent.ON_FOCUS_LOST)] = BaseComponent.ON_FOCUS_LOST,
[hash(BaseComponent.ON_FOCUS_GAINED)] = BaseComponent.ON_FOCUS_GAINED,
[hash(BaseComponent.ON_WINDOW_RESIZED)] = BaseComponent.ON_WINDOW_RESIZED,
[hash(BaseComponent.ON_MESSAGE_INPUT)] = BaseComponent.ON_MESSAGE_INPUT,
[hash(BaseComponent.ON_LANGUAGE_CHANGE)] = BaseComponent.ON_LANGUAGE_CHANGE,
}
local uid = 0 local uid = 0
function BaseComponent.create_uid() ---@private
function M.create_uid()
uid = uid + 1 uid = uid + 1
return uid return uid
end end
--- Set current component style table. ---Set component style. Pass nil to clear style
-- ---@generic T
-- Invoke `on_style_change` on component, if exist. Component should handle ---@param self T
-- their style changing and store all style params ---@param druid_style table|nil
-- @tparam BaseComponent self @{BaseComponent} ---@return T self The component itself for chaining
-- @tparam table|nil druid_style Druid style module function M:set_style(druid_style)
-- @treturn BaseComponent @{BaseComponent} ---@cast self druid.base_component
-- @local
function BaseComponent.set_style(self, druid_style)
self._meta.style = druid_style or {} self._meta.style = druid_style or {}
local component_style = self._meta.style[self._component.name] or {} local component_style = self._meta.style[self._component.name] or {}
@@ -89,21 +67,22 @@ function BaseComponent.set_style(self, druid_style)
end end
--- Set component template name. ---Set component template name. Pass nil to clear template.
-- ---This template id used to access nodes inside the template on GUI scene.
-- Use on all your custom components with GUI layouts used as templates. ---Parent template will be added automatically if exist.
-- It will check parent template name to build full template name in self:get_node() ---@generic T
-- @tparam BaseComponent self @{BaseComponent} ---@param self T
-- @tparam string template BaseComponent template name ---@param template string|nil
-- @treturn BaseComponent @{BaseComponent} ---@return T self The component itself for chaining
-- @local function M:set_template(template)
function BaseComponent.set_template(self, template) ---@cast self druid.base_component
template = template or "" template = template or ""
local parent = self:get_parent_component() local parent = self:get_parent_component()
if parent and IS_AUTO_TEMPLATE then if parent then
local parent_template = parent:get_template() local parent_template = parent:get_template()
if #parent_template > 0 then if parent_template and #parent_template > 0 then
if #template > 0 then if #template > 0 then
template = "/" .. template template = "/" .. template
end end
@@ -111,106 +90,52 @@ function BaseComponent.set_template(self, template)
end end
end end
if template ~= "" then
self._meta.template = template self._meta.template = template
else
self._meta.template = nil
end
return self return self
end end
--- Get current component template name. ---Get full template name.
-- @tparam BaseComponent self @{BaseComponent} ---@return string
-- @treturn string Component full template name function M:get_template()
function BaseComponent.get_template(self)
return self._meta.template return self._meta.template
end end
--- Set current component nodes. ---Set current component nodes, returned from `gui.clone_tree` function.
-- Use if your component nodes was cloned with `gui.clone_tree` and you got the node tree. ---@param nodes table<hash, node>
-- @tparam BaseComponent self @{BaseComponent} ---@return druid.base_component
-- @tparam table nodes BaseComponent nodes table function M:set_nodes(nodes)
-- @treturn BaseComponent @{BaseComponent}
-- @usage
-- local nodes = gui.clone_tree(self.prefab)
-- ... In your component:
-- self:set_nodes(nodes)
-- @local
function BaseComponent.set_nodes(self, nodes)
self._meta.nodes = nodes self._meta.nodes = nodes
-- When we use gui.clone_tree in inner template (template inside other template)
-- this nodes have no id. We have table: hash(correct_id) : hash("")
-- It's wrong and we use this hack to fix this
if nodes then
for id, node in pairs(nodes) do
gui.set_id(node, id)
end
end
return self return self
end end
--- Context used as first arg in all Druid events ---Return current component context
-- ---@return any context Usually it's self of script but can be any other Druid component
-- Context is usually self of gui_script. function M:get_context()
-- @tparam BaseComponent self @{BaseComponent}
-- @treturn table BaseComponent context
function BaseComponent.get_context(self)
return self._meta.context return self._meta.context
end end
--- Increase input priority in input stack ---Get component node by node_id. Respect to current template and nodes.
-- @tparam BaseComponent self @{BaseComponent} ---@param node_id string|node
-- @local ---@return node
function BaseComponent.increase_input_priority(self) function M:get_node(node_id)
helper.deprecated("The component:increase_input_priority is deprecated. Please use component:set_input_priority(druid_const.PRIORITY_INPUT_MAX) instead") return helper.get_node(node_id, self:get_template(), self:get_nodes())
end end
--- Get component node by name. ---Get Druid instance for inner component creation.
-- ---@param template string|nil
-- If component has nodes, node_or_name should be string ---@param nodes table<hash, node>|nil
-- It autopick node by template name or from nodes by gui.clone_tree ---@return druid_instance
-- if they was setup via component:set_nodes, component:set_template. function M:get_druid(template, nodes)
-- If node is not found, the exception will fired
-- @tparam BaseComponent self @{BaseComponent}
-- @tparam string|node node_or_name Node name or node itself
-- @treturn node Gui node
function BaseComponent.get_node(self, node_or_name)
if type(node_or_name) ~= "string" then
-- Assume it's already node from gui.get_node
return node_or_name
end
local template_name = self:get_template()
local nodes = self:__get_nodes()
if #template_name > 0 then
template_name = template_name .. "/"
end
local node
if nodes then
node = nodes[template_name .. node_or_name]
else
node = gui.get_node(template_name .. node_or_name)
end
if not node then
assert(node, "No component with name: " .. (template_name or "") .. (node_or_name or ""))
end
return node
end
--- Get Druid instance for inner component creation.
-- @tparam BaseComponent self @{BaseComponent}
-- @tparam string|nil template The template name
-- @tparam table|nil nodes The nodes table
-- @treturn DruidInstance Druid instance with component context
function BaseComponent.get_druid(self, template, nodes)
local context = { _context = self } local context = { _context = self }
local druid_instance = setmetatable(context, { __index = self._meta.druid }) local druid_instance = setmetatable(context, { __index = self._meta.druid })
@@ -226,39 +151,33 @@ function BaseComponent.get_druid(self, template, nodes)
end end
--- Return component name ---Get component name
-- @tparam BaseComponent self @{BaseComponent} ---@return string name The component name + uid
-- @treturn string The component name function M:get_name()
function BaseComponent.get_name(self) return self._component.name .. M.create_uid()
return self._component.name .. BaseComponent.create_uid()
end end
--- Return parent component name ---Get parent component name
-- @tparam BaseComponent self @{BaseComponent} ---@return string|nil parent_name The parent component name if exist or nil
-- @treturn string|nil The parent component name if exist or bil function M:get_parent_name()
function BaseComponent.get_parent_name(self)
local parent = self:get_parent_component() local parent = self:get_parent_component()
return parent and parent:get_name() return parent and parent:get_name()
end end
--- Return component input priority ---Get component input priority, the bigger number processed first. Default value: 10
-- @tparam BaseComponent self @{BaseComponent} ---@return number
-- @treturn number The component input priority function M:get_input_priority()
function BaseComponent.get_input_priority(self)
return self._component.input_priority return self._component.input_priority
end end
--- Set component input priority ---Set component input priority, the bigger number processed first. Default value: 10
-- ---@param value number
-- Default value: 10 ---@param is_temporary boolean|nil If true, the reset input priority will return to previous value
-- @tparam BaseComponent self @{BaseComponent} ---@return druid.base_component self The component itself for chaining
-- @tparam number value The new input priority value function M:set_input_priority(value, is_temporary)
-- @tparam boolean|nil is_temporary If true, the reset input priority will return to previous value
-- @treturn number The component input priority
function BaseComponent.set_input_priority(self, value, is_temporary)
assert(value) assert(value)
if self._component.input_priority == value then if self._component.input_priority == value then
@@ -281,32 +200,27 @@ function BaseComponent.set_input_priority(self, value, is_temporary)
end end
--- Reset component input priority to default value ---Reset component input priority to it's default value, that was set in `create` function or `set_input_priority`
-- @tparam BaseComponent self @{BaseComponent} ---@return druid.base_component self The component itself for chaining
-- @treturn number The component input priority function M:reset_input_priority()
function BaseComponent.reset_input_priority(self)
self:set_input_priority(self._component.default_input_priority) self:set_input_priority(self._component.default_input_priority)
return self return self
end end
--- Return component UID. ---Get component UID, unique identifier created in component creation order.
-- ---@return number uid The component uid
-- UID generated in component creation order. function M:get_uid()
-- @tparam BaseComponent self @{BaseComponent}
-- @treturn number The component uid
function BaseComponent.get_uid(self)
return self._component._uid return self._component._uid
end end
--- Set component input state. By default it enabled ---Set component input state. By default it's enabled.
-- ---If input is disabled, the component will not receive input events.
-- If input is disabled, the component will not receive input events ---Recursive for all children components.
-- @tparam BaseComponent self @{BaseComponent} ---@param state boolean
-- @tparam boolean|nil state The component input state ---@return druid.base_component self The component itself for chaining
-- @treturn BaseComponent BaseComponent itself function M:set_input_enabled(state)
function BaseComponent.set_input_enabled(self, state)
self._meta.input_enabled = state self._meta.input_enabled = state
for index = 1, #self._meta.children do for index = 1, #self._meta.children do
@@ -317,23 +231,21 @@ function BaseComponent.set_input_enabled(self, state)
end end
--- Return the parent component if exist ---Get parent component
-- @tparam BaseComponent self @{BaseComponent} ---@return druid.base_component|nil parent The parent component if exist or nil
-- @treturn BaseComponent|nil The druid component instance or nil function M:get_parent_component()
function BaseComponent.get_parent_component(self)
return self._meta.parent return self._meta.parent
end end
--- Setup component context and his style table --- Setup component context and his style table
-- @tparam BaseComponent self @{BaseComponent} ---@param druid_instance table The parent druid instance
-- @tparam table druid_instance The parent druid instance ---@param context table Druid context. Usually it is self of script
-- @tparam table context Druid context. Usually it is self of script ---@param style table Druid style module
-- @tparam table style Druid style module ---@param instance_class table The component instance class
-- @tparam table instance_class The component instance class ---@return druid.base_component BaseComponent itself
-- @treturn component BaseComponent itself ---@private
-- @local function M:setup_component(druid_instance, context, style, instance_class)
function BaseComponent.setup_component(self, druid_instance, context, style, instance_class)
self._meta = { self._meta = {
template = "", template = "",
context = context, context = context,
@@ -342,7 +254,7 @@ function BaseComponent.setup_component(self, druid_instance, context, style, ins
druid = druid_instance, druid = druid_instance,
input_enabled = true, input_enabled = true,
children = {}, children = {},
parent = type(context) ~= "userdata" and context, parent = type(context) ~= "userdata" and context --[[@as druid.base_component]],
instance_class = instance_class instance_class = instance_class
} }
@@ -357,62 +269,32 @@ function BaseComponent.setup_component(self, druid_instance, context, style, ins
end end
--- Print log information if debug mode is enabled
-- @tparam BaseComponent self @{BaseComponent}
-- @tparam string message
-- @tparam table context
-- @local
function BaseComponent.log_message(self, message, context)
if not self._component.is_debug then
return
end
print("[" .. self:get_name() .. "]:", message, helper.table_to_string(context))
end
--- Set debug logs for component enabled or disabled
-- @tparam BaseComponent self @{BaseComponent}
-- @tparam boolean|nil is_debug
-- @local
function BaseComponent.set_debug(self, is_debug)
self._component.is_debug = is_debug
end
--- Return true, if input priority was changed --- Return true, if input priority was changed
-- @tparam BaseComponent self @{BaseComponent} ---@private
-- @local function M:_is_input_priority_changed()
function BaseComponent._is_input_priority_changed(self)
return self._component._is_input_priority_changed return self._component._is_input_priority_changed
end end
--- Reset is_input_priority_changed field --- Reset is_input_priority_changed field
-- @tparam BaseComponent self @{BaseComponent} ---@private
-- @local function M:_reset_input_priority_changed()
function BaseComponent._reset_input_priority_changed(self)
self._component._is_input_priority_changed = false self._component._is_input_priority_changed = false
end end
function BaseComponent.__tostring(self)
return self._component.name
end
--- Get current component interests --- Get current component interests
-- @tparam BaseComponent self @{BaseComponent} ---@return table List of component interests
-- @treturn table List of component interests ---@private
-- @local function M:__get_interests()
function BaseComponent.__get_interests(self)
local instance_class = self._meta.instance_class local instance_class = self._meta.instance_class
if INTERESTS[instance_class] then if INTERESTS[instance_class] then
return INTERESTS[instance_class] return INTERESTS[instance_class]
end end
local interests = {} local interests = {}
for index = 1, #BaseComponent.ALL_INTERESTS do for index = 1, #const.ALL_INTERESTS do
local interest = BaseComponent.ALL_INTERESTS[index] local interest = const.ALL_INTERESTS[index]
if self[interest] and type(self[interest]) == "function" then if self[interest] and type(self[interest]) == "function" then
table.insert(interests, interest) table.insert(interests, interest)
end end
@@ -423,47 +305,52 @@ function BaseComponent.__get_interests(self)
end end
--- Get current component nodes ---Get current component nodes
-- @tparam BaseComponent self @{BaseComponent} ---@return table<hash, node>|nil
-- @treturn table BaseComponent nodes table function M:get_nodes()
-- @local
function BaseComponent.__get_nodes(self)
local nodes = self._meta.nodes local nodes = self._meta.nodes
local parent = self:get_parent_component() local parent = self:get_parent_component()
if parent then if parent then
nodes = nodes or parent:__get_nodes() nodes = nodes or parent:get_nodes()
end end
return nodes return nodes
end end
--- Add child to component children list ---Add child to component children list
-- @tparam BaseComponent self @{BaseComponent} ---@generic T: druid.base_component
-- @tparam component child The druid component instance ---@param child T The druid component instance
-- @local ---@return T self The component itself for chaining
function BaseComponent.__add_child(self, child) ---@private
function M:__add_child(child)
table.insert(self._meta.children, child) table.insert(self._meta.children, child)
return self
end end
--- Remove child from component children list
-- @tparam BaseComponent self @{BaseComponent} ---Remove child from component children list
-- @tparam component child The druid component instance ---@generic T: druid.base_component
-- @local ---@param child T The druid component instance
function BaseComponent.__remove_child(self, child) ---@return boolean true if child was removed
---@private
function M:__remove_child(child)
for i = #self._meta.children, 1, -1 do for i = #self._meta.children, 1, -1 do
if self._meta.children[i] == child then if self._meta.children[i] == child then
table.remove(self._meta.children, i) table.remove(self._meta.children, i)
return true return true
end end
end end
return false
end end
--- Return all children components, recursive --- Return all children components, recursive
-- @tparam BaseComponent self @{BaseComponent} ---@return table Array of childrens if the Druid component instance
-- @treturn table Array of childrens if the Druid component instance function M:get_childrens()
function BaseComponent.get_childrens(self)
local childrens = {} local childrens = {}
for i = 1, #self._meta.children do for i = 1, #self._meta.children do
@@ -477,23 +364,21 @@ function BaseComponent.get_childrens(self)
end end
--- Create new component. It will inheritance from basic Druid component. ---Сreate a new component class, which will inherit from the base Druid component.
-- @function BaseComponent.create ---@param name string|nil The name of the component
-- @tparam string name BaseComponent name ---@param input_priority number|nil The input priority. The bigger number processed first. Default value: 10
-- @tparam number|nil input_priority The input priority. The bigger number processed first ---@return druid.base_component
-- @local function M.create(name, input_priority)
function BaseComponent.create(name, input_priority)
local new_class = setmetatable({}, { local new_class = setmetatable({}, {
__index = BaseComponent, __index = M,
__call = function(cls, ...) __call = function(cls, ...)
local self = setmetatable({ local self = setmetatable({
_component = { _component = {
name = name, name = name or "Druid Component",
input_priority = input_priority or const.PRIORITY_INPUT, input_priority = input_priority or const.PRIORITY_INPUT,
default_input_priority = input_priority or const.PRIORITY_INPUT, default_input_priority = input_priority or const.PRIORITY_INPUT,
is_debug = false,
_is_input_priority_changed = true, -- Default true for sort once time after GUI init _is_input_priority_changed = true, -- Default true for sort once time after GUI init
_uid = BaseComponent.create_uid() _uid = M.create_uid()
} }
}, { }, {
__index = cls __index = cls
@@ -506,4 +391,4 @@ function BaseComponent.create(name, input_priority)
end end
return BaseComponent return M

View File

@@ -1,10 +1,4 @@
-- Copyright (c) 2021 Maksim Tuprikov <insality@gmail.com>. This code is licensed under MIT license ---@class druid.system.const
--- Druid constants
-- @local
-- @module DruidConst
-- @alias druid_const
local M = {} local M = {}
M.ACTION_TEXT = hash(sys.get_config_string("druid.input_text", "text")) M.ACTION_TEXT = hash(sys.get_config_string("druid.input_text", "text"))
@@ -23,9 +17,6 @@ M.ACTION_LSHIFT = hash(sys.get_config_string("druid.input_key_lshift", "key_lshi
M.ACTION_LCTRL = hash(sys.get_config_string("druid.input_key_lctrl", "key_lctrl")) M.ACTION_LCTRL = hash(sys.get_config_string("druid.input_key_lctrl", "key_lctrl"))
M.ACTION_LCMD = hash(sys.get_config_string("druid.input_key_lsuper", "key_lsuper")) M.ACTION_LCMD = hash(sys.get_config_string("druid.input_key_lsuper", "key_lsuper"))
M.IS_STENCIL_CHECK = not (sys.get_config_int("druid.no_stencil_check", 0) == 1)
M.ON_INPUT = "on_input" M.ON_INPUT = "on_input"
M.ON_UPDATE = "update" M.ON_UPDATE = "update"
M.ON_MESSAGE = "on_message" M.ON_MESSAGE = "on_message"
@@ -33,23 +24,28 @@ M.ON_LATE_INIT = "on_late_init"
M.ON_FOCUS_LOST = "on_focus_lost" M.ON_FOCUS_LOST = "on_focus_lost"
M.ON_FOCUS_GAINED = "on_focus_gained" M.ON_FOCUS_GAINED = "on_focus_gained"
M.ON_LAYOUT_CHANGE = "on_layout_change" M.ON_LAYOUT_CHANGE = "on_layout_change"
M.ON_MESSAGE_INPUT = "on_message_input"
M.ON_WINDOW_RESIZED = "on_window_resized" M.ON_WINDOW_RESIZED = "on_window_resized"
M.ON_LANGUAGE_CHANGE = "on_language_change" M.ON_LANGUAGE_CHANGE = "on_language_change"
M.ALL_INTERESTS = {
M.ON_INPUT,
M.ON_UPDATE,
M.ON_MESSAGE,
M.ON_LATE_INIT,
M.ON_FOCUS_LOST,
M.ON_FOCUS_GAINED,
M.ON_LAYOUT_CHANGE,
M.ON_WINDOW_RESIZED,
M.ON_LANGUAGE_CHANGE,
}
M.MSG_LAYOUT_CHANGED = hash("layout_changed")
-- Components with higher priority value processed first -- Components with higher priority value processed first
M.PRIORITY_INPUT = 10 M.PRIORITY_INPUT = 10
M.PRIORITY_INPUT_HIGH = 20 M.PRIORITY_INPUT_HIGH = 20
M.PRIORITY_INPUT_MAX = 100 M.PRIORITY_INPUT_MAX = 100
M.MESSAGE_INPUT = {
BUTTON_CLICK = "button_click",
BUTTON_LONG_CLICK = "button_long_click",
BUTTON_DOUBLE_CLICK = "button_double_click",
BUTTON_REPEATED_CLICK = "button_repeated_click",
TEXT_SET = "text_set",
}
M.PIVOTS = { M.PIVOTS = {
[gui.PIVOT_CENTER] = vmath.vector3(0), [gui.PIVOT_CENTER] = vmath.vector3(0),
[gui.PIVOT_N] = vmath.vector3(0, 0.5, 0), [gui.PIVOT_N] = vmath.vector3(0, 0.5, 0),
@@ -83,7 +79,6 @@ M.LAYOUT_MODE = {
STRETCH = gui.ADJUST_STRETCH, STRETCH = gui.ADJUST_STRETCH,
} }
M.VECTOR_ZERO = vmath.vector3(0)
M.SYS_INFO = sys.get_sys_info() M.SYS_INFO = sys.get_sys_info()
M.CURRENT_SYSTEM_NAME = M.SYS_INFO.system_name M.CURRENT_SYSTEM_NAME = M.SYS_INFO.system_name
@@ -104,10 +99,13 @@ M.SHIFT = {
M.TEXT_ADJUST = { M.TEXT_ADJUST = {
DOWNSCALE = "downscale", DOWNSCALE = "downscale",
TRIM = "trim",
NO_ADJUST = "no_adjust", NO_ADJUST = "no_adjust",
DOWNSCALE_LIMITED = "downscale_limited", DOWNSCALE_LIMITED = "downscale_limited",
SCROLL = "scroll", SCROLL = "scroll",
TRIM = "trim",
TRIM_LEFT = "trim_left",
SCALE_THEN_TRIM = "scale_then_trim",
SCALE_THEN_TRIM_LEFT = "scale_then_trim_left",
SCALE_THEN_SCROLL = "scale_then_scroll", SCALE_THEN_SCROLL = "scale_then_scroll",
} }
@@ -116,17 +114,5 @@ M.SIDE = {
Y = "y" Y = "y"
} }
M.SWIPE = {
UP = "up",
DOWN = "down",
LEFT = "left",
RIGHT = "right",
}
M.ERRORS = {
GRID_DYNAMIC_ANCHOR = "The pivot of dynamic grid node should be West, East, South or North"
}
M.EMPTY_FUNCTION = function() end
return M return M

View File

@@ -1,371 +1,160 @@
script: ""
fonts { fonts {
name: "game" name: "druid_text_bold"
font: "/example/assets/fonts/game.font" font: "/druid/fonts/druid_text_bold.font"
} }
textures { textures {
name: "kenney" name: "druid"
texture: "/example/assets/images/kenney.atlas" texture: "/druid/druid.atlas"
}
background_color {
x: 0.0
y: 0.0
z: 0.0
w: 0.0
} }
nodes { nodes {
position {
x: 0.0
y: 0.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
}
size { size {
x: 1.0 x: 200.0
y: 1.0 y: 40.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
} }
type: TYPE_BOX type: TYPE_BOX
blend_mode: BLEND_MODE_ALPHA
texture: "kenney/empty"
id: "root" id: "root"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
adjust_mode: ADJUST_MODE_FIT
layer: ""
inherit_alpha: true inherit_alpha: true
slice9 { visible: false
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_AUTO
} }
nodes { nodes {
position {
x: 0.0
y: 0.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
}
size { size {
x: 190.0 x: 200.0
y: 45.0 y: 40.0
z: 0.0
w: 1.0
} }
color { color {
x: 1.0 x: 0.31
y: 1.0 y: 0.318
z: 1.0 z: 0.322
w: 1.0
} }
type: TYPE_BOX type: TYPE_BOX
blend_mode: BLEND_MODE_ALPHA texture: "druid/rect_round2_width2"
texture: "kenney/progress_back"
id: "button" id: "button"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
adjust_mode: ADJUST_MODE_FIT
parent: "root" parent: "root"
layer: ""
inherit_alpha: true inherit_alpha: true
slice9 { slice9 {
x: 0.0 x: 4.0
y: 0.0 y: 4.0
z: 0.0 z: 4.0
w: 0.0 w: 4.0
} }
clipping_mode: CLIPPING_MODE_NONE
clipping_visible: true
clipping_inverted: false
alpha: 1.0
template_node_child: false
size_mode: SIZE_MODE_AUTO
} }
nodes { nodes {
position {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale { scale {
x: 0.5 x: 0.5
y: 0.5 y: 0.5
z: 1.0
w: 1.0
} }
size { size {
x: 300.0 x: 380.0
y: 60.0 y: 50.0
z: 0.0
w: 1.0
} }
color { color {
x: 0.9490196 x: 0.31
y: 0.9490196 y: 0.318
z: 0.9490196 z: 0.322
w: 1.0
} }
type: TYPE_TEXT type: TYPE_TEXT
blend_mode: BLEND_MODE_ALPHA
text: "Placeholder" text: "Placeholder"
font: "game" font: "druid_text_bold"
id: "placeholder_text" id: "placeholder_text"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
outline { outline {
x: 0.4 x: 0.4
y: 0.4 y: 0.4
z: 0.4 z: 0.4
w: 1.0
} }
shadow { shadow {
x: 1.0 x: 1.0
y: 1.0 y: 1.0
z: 1.0 z: 1.0
w: 1.0
} }
adjust_mode: ADJUST_MODE_FIT parent: "root"
line_break: false
parent: "button"
layer: ""
inherit_alpha: true inherit_alpha: true
alpha: 1.0 outline_alpha: 0.0
outline_alpha: 1.0
shadow_alpha: 0.0 shadow_alpha: 0.0
template_node_child: false
text_leading: 1.0
text_tracking: 0.0
} }
nodes { nodes {
position {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale { scale {
x: 0.6 x: 0.5
y: 0.6 y: 0.5
z: 1.0
w: 1.0
} }
size { size {
x: 300.0 x: 380.0
y: 60.0 y: 50.0
z: 0.0
w: 1.0
} }
color { color {
x: 1.0 x: 0.722
y: 1.0 y: 0.741
z: 1.0 z: 0.761
w: 1.0
} }
type: TYPE_TEXT type: TYPE_TEXT
blend_mode: BLEND_MODE_ALPHA
text: "User input" text: "User input"
font: "game" font: "druid_text_bold"
id: "input_text" id: "input_text"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
outline {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
shadow { shadow {
x: 1.0 x: 1.0
y: 1.0 y: 1.0
z: 1.0 z: 1.0
w: 1.0
} }
adjust_mode: ADJUST_MODE_FIT parent: "root"
line_break: false
parent: "button"
layer: ""
inherit_alpha: true inherit_alpha: true
alpha: 1.0 outline_alpha: 0.0
outline_alpha: 1.0
shadow_alpha: 0.0 shadow_alpha: 0.0
template_node_child: false
text_leading: 1.0
text_tracking: 0.0
} }
nodes { nodes {
position { position {
x: 67.0 x: 61.0
y: 0.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
} }
scale { scale {
x: 0.6 x: 0.5
y: 0.6 y: 0.5
z: 1.0
w: 1.0
} }
size { size {
x: 1.0 x: 16.0
y: 1.0 y: 50.0
z: 0.0
w: 1.0
} }
color { color {
x: 1.0 x: 0.631
y: 1.0 y: 0.843
z: 1.0 z: 0.961
w: 1.0
} }
type: TYPE_BOX type: TYPE_BOX
blend_mode: BLEND_MODE_ALPHA texture: "druid/ui_circle_16"
texture: "kenney/empty"
id: "cursor_node" id: "cursor_node"
xanchor: XANCHOR_NONE parent: "root"
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
adjust_mode: ADJUST_MODE_FIT
parent: "button"
layer: ""
inherit_alpha: true inherit_alpha: true
slice9 { slice9 {
x: 0.0 x: 8.0
y: 0.0 y: 8.0
z: 0.0 z: 8.0
w: 0.0 w: 8.0
} }
clipping_mode: CLIPPING_MODE_NONE alpha: 0.5
clipping_visible: true
clipping_inverted: false
alpha: 1.0
template_node_child: false
size_mode: SIZE_MODE_AUTO
} }
nodes { nodes {
position { position {
x: 0.0 x: -1.4
y: 2.0 y: 4.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
} }
size { size {
x: 20.0 x: 20.0
y: 40.0 y: 40.0
z: 0.0
w: 1.0
} }
color { color {
x: 0.2 x: 0.722
y: 0.2 y: 0.741
z: 0.2 z: 0.761
w: 1.0
} }
type: TYPE_TEXT type: TYPE_TEXT
blend_mode: BLEND_MODE_ALPHA
text: "|" text: "|"
font: "game" font: "druid_text_bold"
id: "cursor_text" id: "cursor_text"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
outline {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
shadow { shadow {
x: 1.0 x: 1.0
y: 1.0 y: 1.0
z: 1.0 z: 1.0
w: 1.0
} }
adjust_mode: ADJUST_MODE_FIT
line_break: false
parent: "cursor_node" parent: "cursor_node"
layer: ""
inherit_alpha: true
alpha: 1.0
outline_alpha: 0.0 outline_alpha: 0.0
shadow_alpha: 0.0 shadow_alpha: 0.0
template_node_child: false
text_leading: 1.0
text_tracking: 0.0
} }
material: "/builtins/materials/gui.material" material: "/builtins/materials/gui.material"
adjust_reference: ADJUST_REFERENCE_PARENT adjust_reference: ADJUST_REFERENCE_PARENT
max_nodes: 512

View File

@@ -6,13 +6,13 @@
-- @alias druid.rich_input -- @alias druid.rich_input
--- The component druid instance --- The component druid instance
-- @tfield DruidInstance druid @{DruidInstance} -- @tfield DruidInstance druid DruidInstance
--- Root node --- Root node
-- @tfield node root -- @tfield node root
--- On input field text change callback(self, input_text) --- On input field text change callback(self, input_text)
-- @tfield Input input @{Input} -- @tfield Input input Input
--- On input field text change to empty string callback(self, input_text) --- On input field text change to empty string callback(self, input_text)
-- @tfield node cursor -- @tfield node cursor
@@ -43,17 +43,22 @@ local const = require("druid.const")
local utf8_lua = require("druid.system.utf8") local utf8_lua = require("druid.system.utf8")
local utf8 = utf8 or utf8_lua local utf8 = utf8 or utf8_lua
local input = require("druid.extended.input") ---@class druid.rich_input: druid.base_component
local RichInput = component.create("druid.rich_input") ---@field root node
---@field input druid.input
---@field cursor node
---@field cursor_text node
---@field cursor_position vector3
local M = component.create("druid.rich_input")
local SCHEME = { --local SCHEME = {
ROOT = "root", -- ROOT = "root",
BUTTON = "button", -- BUTTON = "button",
PLACEHOLDER = "placeholder_text", -- PLACEHOLDER = "placeholder_text",
INPUT = "input_text", -- INPUT = "input_text",
CURSOR = "cursor_node", -- CURSOR = "cursor_node",
CURSOR_TEXT = "cursor_text", -- CURSOR_TEXT = "cursor_text",
} --}
local DOUBLE_CLICK_TIME = 0.35 local DOUBLE_CLICK_TIME = 0.35
@@ -189,13 +194,11 @@ local function on_drag_callback(self, dx, dy, x, y, touch)
end end
--- The @{RichInput} constructor ---@param template string The template string name
-- @tparam RichInput self @{RichInput} ---@param nodes table Nodes table from gui.clone_tree
-- @tparam string template The template string name function M:init(template, nodes)
-- @tparam table nodes Nodes table from gui.clone_tree
function RichInput.init(self, template, nodes)
self.druid = self:get_druid(template, nodes) self.druid = self:get_druid(template, nodes)
self.root = self:get_node(SCHEME.ROOT) self.root = self:get_node("root")
self._last_touch_info = { self._last_touch_info = {
cursor_index = nil, cursor_index = nil,
@@ -204,20 +207,20 @@ function RichInput.init(self, template, nodes)
self.is_lshift = false self.is_lshift = false
self.is_lctrl = false self.is_lctrl = false
self.input = self.druid:new(input, self:get_node(SCHEME.BUTTON), self:get_node(SCHEME.INPUT)) self.input = self.druid:new_input("button", "input_text")
self.is_button_input_enabled = gui.is_enabled(self.input.button.node) self.is_button_input_enabled = gui.is_enabled(self.input.button.node)
self.cursor = self:get_node(SCHEME.CURSOR) self.cursor = self:get_node("cursor_node")
self.cursor_position = gui.get_position(self.cursor) self.cursor_position = gui.get_position(self.cursor)
self.cursor_text = self:get_node(SCHEME.CURSOR_TEXT) self.cursor_text = self:get_node("cursor_text")
self.drag = self.druid:new_drag(self:get_node(SCHEME.BUTTON), on_drag_callback) self.drag = self.druid:new_drag("button", on_drag_callback)
self.drag.on_touch_start:subscribe(on_touch_start_callback) self.drag.on_touch_start:subscribe(on_touch_start_callback)
self.drag:set_input_priority(const.PRIORITY_INPUT_MAX + 1) self.drag:set_input_priority(const.PRIORITY_INPUT_MAX + 1)
self.drag:set_enabled(false) self.drag:set_enabled(false)
self.input:set_text("") self.input:set_text("")
self.placeholder = self.druid:new_text(self:get_node(SCHEME.PLACEHOLDER)) self.placeholder = self.druid:new_text("placeholder_text")
self.text_position = gui.get_position(self.input.text.node) self.text_position = gui.get_position(self.input.text.node)
self.input.on_input_text:subscribe(update_text) self.input.on_input_text:subscribe(update_text)
@@ -230,7 +233,7 @@ function RichInput.init(self, template, nodes)
end end
function RichInput.on_input(self, action_id, action) function M:on_input(action_id, action)
if action_id == const.ACTION_LSHIFT then if action_id == const.ACTION_LSHIFT then
if action.pressed then if action.pressed then
self.is_lshift = true self.is_lshift = true
@@ -247,37 +250,38 @@ function RichInput.on_input(self, action_id, action)
end end
end end
if self.input.is_selected then
if action_id == const.ACTION_LEFT and (action.pressed or action.repeated) then if action_id == const.ACTION_LEFT and (action.pressed or action.repeated) then
self.input:move_selection(-1, self.is_lshift, self.is_lctrl) self.input:move_selection(-1, self.is_lshift, self.is_lctrl)
return true
end end
if action_id == const.ACTION_RIGHT and (action.pressed or action.repeated) then if action_id == const.ACTION_RIGHT and (action.pressed or action.repeated) then
self.input:move_selection(1, self.is_lshift, self.is_lctrl) self.input:move_selection(1, self.is_lshift, self.is_lctrl)
return true
end
end end
end end
--- Set placeholder text --- Set placeholder text
-- @tparam RichInput self @{RichInput} ---@param placeholder_text string The placeholder text
-- @tparam string placeholder_text The placeholder text function M:set_placeholder(placeholder_text)
function RichInput.set_placeholder(self, placeholder_text) self.placeholder:set_text(placeholder_text)
self.placeholder:set_to(placeholder_text)
return self return self
end end
--- Select input field --- Select input field
-- @tparam RichInput self @{RichInput} function M:select()
function RichInput.select(self)
self.input:select() self.input:select()
end end
--- Set input field text --- Set input field text
-- @tparam RichInput self @{RichInput} ---@param text string The input text
-- @treturn druid.input Current input instance ---@return druid.rich_input self Current instance
-- @tparam string text The input text function M:set_text(text)
function RichInput.set_text(self, text)
self.input:set_text(text) self.input:set_text(text)
gui.set_enabled(self.placeholder.node, true and #self.input:get_text() == 0) gui.set_enabled(self.placeholder.node, true and #self.input:get_text() == 0)
@@ -286,10 +290,9 @@ end
--- Set input field font --- Set input field font
-- @tparam RichInput self @{RichInput} ---@param font hash The font hash
-- @tparam hash font The font hash ---@return druid.rich_input self Current instance
-- @treturn druid.input Current input instance function M:set_font(font)
function RichInput.set_font(self, font)
gui.set_font(self.input.text.node, font) gui.set_font(self.input.text.node, font)
gui.set_font(self.placeholder.node, font) gui.set_font(self.placeholder.node, font)
@@ -298,8 +301,7 @@ end
--- Set input field text --- Set input field text
-- @tparam RichInput self @{RichInput} function M:get_text()
function RichInput.get_text(self)
return self.input:get_text() return self.input:get_text()
end end
@@ -307,14 +309,13 @@ end
--- Set allowed charaters for input field. --- Set allowed charaters for input field.
-- See: https://defold.com/ref/stable/string/ -- See: https://defold.com/ref/stable/string/
-- ex: [%a%d] for alpha and numeric -- ex: [%a%d] for alpha and numeric
-- @tparam RichInput self @{RichInput} ---@param characters string Regulax exp. for validate user input
-- @tparam string characters Regulax exp. for validate user input ---@return druid.rich_input Current instance
-- @treturn RichInput Current instance function M:set_allowed_characters(characters)
function RichInput.set_allowed_characters(self, characters)
self.input:set_allowed_characters(characters) self.input:set_allowed_characters(characters)
return self return self
end end
return RichInput return M

View File

@@ -2,10 +2,6 @@
-- Author: Britzl -- Author: Britzl
-- Modified by: Insality -- Modified by: Insality
--- RT
-- @module rich_text.rt
-- @local
local helper = require("druid.helper") local helper = require("druid.helper")
local parser = require("druid.custom.rich_text.module.rt_parse") local parser = require("druid.custom.rich_text.module.rt_parse")
local utf8_lua = require("druid.system.utf8") local utf8_lua = require("druid.system.utf8")
@@ -185,7 +181,7 @@ function M.create(text, settings, style)
outline = settings.outline, outline = settings.outline,
font = gui.get_font(settings.text_prefab), font = gui.get_font(settings.text_prefab),
-- Image params -- Image params
---@type druid.rich_text.image ---@type druid.rich_text.word.image
image = nil, image = nil,
-- Tags -- Tags
br = nil, br = nil,
@@ -428,7 +424,7 @@ function M._update_nodes(lines, settings)
gui.set_outline(node, word.outline) gui.set_outline(node, word.outline)
gui.set_shadow(node, word.shadow) gui.set_shadow(node, word.shadow)
gui.set_text(node, word.text) gui.set_text(node, word.text)
gui.set_color(node, word.color or word.text_color) gui.set_color(node, word.color)
gui.set_font(node, word.font or settings.font) gui.set_font(node, word.font or settings.font)
end end
word.node = node word.node = node

View File

@@ -63,7 +63,7 @@
-- @alias druid.rich_text -- @alias druid.rich_text
--- The component druid instance --- The component druid instance
-- @tfield DruidInstance druid @{DruidInstance} -- @tfield DruidInstance druid DruidInstance
--- The root node of the Rich Text --- The root node of the Rich Text
-- @tfield node root -- @tfield node root
@@ -76,14 +76,83 @@
local component = require("druid.component") local component = require("druid.component")
local rich_text = require("druid.custom.rich_text.module.rt") local rich_text = require("druid.custom.rich_text.module.rt")
local RichText = component.create("rich_text") ---@class druid.rich_text.settings
---@field parent node
---@field size number
---@field fonts table<string, string>
---@field scale vector3
---@field color vector4
---@field shadow vector4
---@field outline vector4
---@field position vector3
---@field image_pixel_grid_snap boolean
---@field combine_words boolean
---@field default_animation string
---@field text_prefab node
---@field adjust_scale number
---@field default_texture string
---@field is_multiline boolean
---@field text_leading number
---@field font hash
---@field width number
---@field height number
---@class druid.rich_text.word
---@field node node
---@field relative_scale number
---@field source_text string
---@field color vector4
---@field position vector3
---@field offset vector3
---@field scale vector3
---@field size vector3
---@field metrics druid.rich_text.metrics
---@field pivot userdata
---@field text string
---@field shadow vector4
---@field outline vector4
---@field font string
---@field image druid.rich_text.word.image
---@field br boolean
---@field nobr boolean
---@class druid.rich_text.word.image
---@field texture string
---@field anim string
---@field width number
---@field height number
---@class druid.rich_text.style
---@field COLORS table<string, vector4>
---@field ADJUST_STEPS number
---@field ADJUST_SCALE_DELTA number
---@field ADJUST_TYPE string
---@field ADJUST_SCALE number
---@class druid.rich_text.lines_metrics
---@field text_width number
---@field text_height number
---@field lines table<number, druid.rich_text.metrics>
---@class druid.rich_text.metrics
---@field width number
---@field height number
---@field offset_x number|nil
---@field offset_y number|nil
---@field node_size vector3|nil
---@class druid.rich_text: druid.base_component
---@field root node
---@field text_prefab node
---@field private _last_value string
---@field private _settings table
local M = component.create("rich_text")
--- The @{RichText} constructor --- The RichText constructor
-- @tparam RichText self @{RichText} ---@param text_node node|string The text node to make Rich Text
-- @tparam node|string text_node The text node to make Rich Text ---@param value string|nil The initial text value. Default will be gui.get_text(text_node)
-- @tparam string|nil value The initial text value. Default will be gui.get_text(text_node) function M:init(text_node, value)
function RichText.init(self, text_node, value)
self.root = self:get_node(text_node) self.root = self:get_node(text_node)
self.text_prefab = self.root self.text_prefab = self.root
@@ -98,7 +167,7 @@ function RichText.init(self, text_node, value)
end end
function RichText.on_layout_change(self) function M:on_layout_change()
if self._last_value then if self._last_value then
self:set_text(self._last_value) self:set_text(self._last_value)
end end
@@ -112,7 +181,7 @@ end
-- @tfield table|nil COLORS Rich Text color aliases. Default: {} -- @tfield table|nil COLORS Rich Text color aliases. Default: {}
-- @tfield number|nil ADJUST_STEPS Amount steps of attemps text adjust by height. Default: 20 -- @tfield number|nil ADJUST_STEPS Amount steps of attemps text adjust by height. Default: 20
-- @tfield number|nil ADJUST_SCALE_DELTA Scale step on each height adjust step. Default: 0.02 -- @tfield number|nil ADJUST_SCALE_DELTA Scale step on each height adjust step. Default: 0.02
function RichText.on_style_change(self, style) function M:on_style_change(style)
self.style = {} self.style = {}
self.style.COLORS = style.COLORS or {} self.style.COLORS = style.COLORS or {}
self.style.ADJUST_STEPS = style.ADJUST_STEPS or 20 self.style.ADJUST_STEPS = style.ADJUST_STEPS or 20
@@ -121,10 +190,9 @@ end
--- Set text for Rich Text --- Set text for Rich Text
-- @tparam RichText self @{RichText} ---@param text string|nil The text to set
-- @tparam string|nil text The text to set ---@return druid.rich_text.word[] words
-- @treturn druid.rich_text.word[] words ---@return druid.rich_text.lines_metrics line_metrics
-- @treturn druid.rich_text.lines_metrics line_metrics
-- @usage -- @usage
-- • color: Change text color -- • color: Change text color
-- --
@@ -168,7 +236,7 @@ end
-- <img=texture:image/> -- <img=texture:image/>
-- <img=texture:image,size/> -- <img=texture:image,size/>
-- <img=texture:image,width,height/> -- <img=texture:image,width,height/>
function RichText.set_text(self, text) function M:set_text(text)
text = text or "" text = text or ""
self:clear() self:clear()
self._last_value = text self._last_value = text
@@ -184,14 +252,13 @@ end
--- Get current text --- Get current text
-- @tparam RichText self @{RichText} ---@return string text
-- @treturn string text function M:get_text()
function RichText.get_text(self)
return self._last_value return self._last_value
end end
function RichText:on_remove() function M:on_remove()
gui.set_scale(self.root, self._default_scale) gui.set_scale(self.root, self._default_scale)
gui.set_size(self.root, self._default_size) gui.set_size(self.root, self._default_size)
self:clear() self:clear()
@@ -199,7 +266,7 @@ end
--- Clear all created words. --- Clear all created words.
function RichText:clear() function M:clear()
if self._words then if self._words then
rich_text.remove(self._words) rich_text.remove(self._words)
self._words = nil self._words = nil
@@ -209,12 +276,11 @@ end
--- Get all words, which has a passed tag. --- Get all words, which has a passed tag.
-- @tparam RichText self @{RichText} ---@param tag string
-- @tparam string tag ---@return druid.rich_text.word[] words
-- @treturn druid.rich_text.word[] words function M:tagged(tag)
function RichText.tagged(self, tag)
if not self._words then if not self._words then
return return {}
end end
return rich_text.tagged(self._words, tag) return rich_text.tagged(self._words, tag)
@@ -222,29 +288,28 @@ end
---Split a word into it's characters ---Split a word into it's characters
-- @tparam RichText self @{RichText} ---@param word druid.rich_text.word
-- @tparam druid.rich_text.word word ---@return druid.rich_text.word[] characters
-- @treturn druid.rich_text.word[] characters function M:characters(word)
function RichText.characters(self, word)
return rich_text.characters(word) return rich_text.characters(word)
end end
--- Get all current words. --- Get all current words.
-- @treturn table druid.rich_text.word[] ---@return druid.rich_text.word[]
function RichText:get_words() function M:get_words()
return self._words return self._words
end end
--- Get current line metrics --- Get current line metrics
--- @treturn druid.rich_text.lines_metrics ----@return druid.rich_text.lines_metrics
function RichText:get_line_metric() function M:get_line_metric()
return self._line_metrics return self._line_metrics
end end
function RichText:_create_settings() function M:_create_settings()
local root_size = gui.get_size(self.root) local root_size = gui.get_size(self.root)
local scale = gui.get_scale(self.root) local scale = gui.get_scale(self.root)
@@ -280,4 +345,4 @@ function RichText:_create_settings()
end end
return RichText return M

View File

@@ -19,4 +19,13 @@ images {
images { images {
image: "/druid/images/pixel.png" image: "/druid/images/pixel.png"
} }
images {
image: "/druid/images/panels/rect_round2_width2.png"
}
images {
image: "/druid/images/icons/icon_drag.png"
}
images {
image: "/druid/images/icons/icon_arrow.png"
}
extrude_borders: 2 extrude_borders: 2

View File

@@ -1,115 +1,18 @@
-- Copyright (c) 2021 Maksim Tuprikov <insality@gmail.com>. This code is licensed under MIT license local events = require("event.events")
--- Druid UI Component Framework.
-- <b># Overview #</b>
--
-- Druid - powerful Defold component UI library. Use basic and extended
-- Druid components or make your own game-specific components to make
-- amazing GUI in your games.
--
-- To start using Druid, please refer to the Usage section below.
--
-- <b># Notes #</b>
--
-- • Each Druid instance maintains the self context from the constructor and passes it to each Druid callback.
--
-- See next: @{DruidInstance}
--
-- @usage
-- local druid = require("druid.druid")
--
-- local function on_play(self)
-- print("Gonna play!")
-- end
--
-- function init(self)
-- self.druid = druid.new(self)
-- self.druid:new_button("button_play", on_play)
-- end
--
-- function final(self)
-- self.druid:final()
-- end
--
-- function update(self, dt)
-- self.druid:update(dt)
-- end
--
-- function on_message(self, message_id, message, sender)
-- self.druid:on_message(message_id, message, sender)
-- end
--
-- function on_input(self, action_id, action)
-- return self.druid:on_input(action_id, action)
-- end
--
-- @module Druid
local const = require("druid.const")
local base_component = require("druid.component")
local settings = require("druid.system.settings") local settings = require("druid.system.settings")
local druid_instance = require("druid.system.druid_instance") local druid_instance = require("druid.system.druid_instance")
local default_style = require("druid.styles.default.style") local default_style = require("druid.styles.default.style")
---@class druid
local M = {} local M = {}
local _instances = {}
---Create a new Druid instance for creating GUI components.
local function clean_deleted_druid_instances() ---@param context table The Druid context. Usually, this is the self of the gui_script. It is passed into all Druid callbacks.
for i = #_instances, 1, -1 do ---@param style table|nil The Druid style table to override style parameters for this Druid instance.
if _instances[i]._deleted then ---@return druid_instance druid_instance The new Druid instance
table.remove(_instances, i)
end
end
end
local function get_druid_instances()
clean_deleted_druid_instances()
return _instances
end
--- Register a new external Druid component.
--
-- You can register your own components to make new alias: the druid:new_{name} function.
-- For example, if you want to register a component called "my_component", you can create it using druid:new_my_component(...).
-- This can be useful if you have your own "basic" components that you don't want to re-create each time.
-- @function druid.register
-- @tparam string name module name
-- @tparam table module lua table with component
-- @usage
-- local my_component = require("path.to.my.component")
-- druid.register("my_component", my_component)
-- ...
-- local druid = druid.new(self)
-- local component_instance = self.druid:new_my_component(...)
function M.register(name, module)
druid_instance["new_" .. name] = function(self, ...)
return druid_instance.new(self, module, ...)
end
return druid_instance["new_" .. name]
end
--- Create a new Druid instance for creating GUI components.
--
-- @function druid.new
-- @tparam table context The Druid context. Usually, this is the self of the gui_script. It is passed into all Druid callbacks.
-- @tparam table|nil style The Druid style table to override style parameters for this Druid instance.
-- @treturn druid_instance The Druid instance @{DruidInstance}.
-- @usage
-- local druid = require("druid.druid")
--
-- function init(self)
-- self.druid = druid.new(self)
-- end
function M.new(context, style) function M.new(context, style)
clean_deleted_druid_instances()
if settings.default_style == nil then if settings.default_style == nil then
M.set_default_style(default_style) M.set_default_style(default_style)
end end
@@ -117,96 +20,66 @@ function M.new(context, style)
local new_instance = setmetatable({}, { __index = druid_instance }) local new_instance = setmetatable({}, { __index = druid_instance })
new_instance:initialize(context, style) new_instance:initialize(context, style)
table.insert(_instances, new_instance)
return new_instance return new_instance
end end
--- Set your own default style for all Druid instances. ---Register a new external Druid component.
-- ---Register component just makes the druid:new_{name} function.
-- To create your own style file, copy the default style file and make changes to it. ---For example, if you register a component called "my_component", you can create it using druid:new_my_component(...).
-- Register the new style before creating your Druid instances. ---This can be useful if you have your own "basic" components that you don't want to require in every file.
-- @function druid.set_default_style ---The default way to create component is `druid_instance:new(component_class, ...)`.
-- @tparam table style Druid style module ---@param name string Module name
-- @usage ---@param module table Lua table with component
-- local my_style = require("path.to.my.style") function M.register(name, module)
-- druid.set_default_style(my_style) druid_instance["new_" .. name] = function(self, ...)
return druid_instance.new(self, module, ...)
end
end
---Set the default style for all Druid instances.
---@param style table Default style
function M.set_default_style(style) function M.set_default_style(style)
settings.default_style = style or {} settings.default_style = style or {}
end end
--- Set the text function for the LangText component. ---Set the text function for the LangText component.
-- ---@param callback fun(text_id: string): string Get localized text function
-- The Druid locale component will call this function to get translated text.
-- After setting the text function, all existing locale components will be updated.
-- @function druid.set_text_function
-- @tparam function callback Get localized text function
-- @usage
-- druid.set_text_function(function(text_id)
-- return lang_data[text_id] -- Replace with your real function
-- end)
function M.set_text_function(callback) function M.set_text_function(callback)
settings.get_text = callback or const.EMPTY_FUNCTION settings.get_text = callback or function() end
M.on_language_change() M.on_language_change()
end end
--- Set the Druid sound function to play UI sounds if used. ---Set the sound function to able components to play sounds.
-- ---@param callback fun(sound_id: string) Sound play callback
-- Set a function to play a sound given a sound_id. This function is used for button clicks to play the "click" sound.
-- It can also be used to play sounds in your custom components (see the default Druid style file for an example).
-- @function druid.set_sound_function
-- @tparam function callback Sound play callback
-- @usage
-- druid.set_sound_function(function(sound_id)
-- sound.play(sound_id) -- Replace with your real function
-- end)
function M.set_sound_function(callback) function M.set_sound_function(callback)
settings.play_sound = callback or const.EMPTY_FUNCTION settings.play_sound = callback or function() end
end end
--- Set the window callback to enable on_focus_gain and on_focus_lost functions. ---Subscribe Druid to the window listener. It will override your previous
-- ---window listener, so if you have one, you should call M.on_window_callback manually.
-- This is used to trigger the on_focus_lost and on_focus_gain functions in Druid components. function M.init_window_listener()
-- @function druid.on_window_callback window.set_listener(function(_, window_event)
-- @tparam string event Event param from window listener events.trigger("druid.window_event", window_event)
-- @usage end)
-- window.set_listener(function(_, event)
-- druid.on_window_callback(event)
-- end)
function M.on_window_callback(event)
local instances = get_druid_instances()
if event == window.WINDOW_EVENT_FOCUS_LOST then
for i = 1, #instances do
msg.post(instances[i].url, base_component.ON_FOCUS_LOST)
end
elseif event == window.WINDOW_EVENT_FOCUS_GAINED then
for i = 1, #instances do
msg.post(instances[i].url, base_component.ON_FOCUS_GAINED)
end
elseif event == window.WINDOW_EVENT_RESIZED then
for i = 1, #instances do
msg.post(instances[i].url, base_component.ON_WINDOW_RESIZED)
end
end
end end
--- Call this function when the game language changes. ---Set the window callback to enable Druid window events.
-- ---@param window_event constant Event param from window listener
-- This function will translate all current LangText components. function M.on_window_callback(window_event)
-- @function druid.on_language_change events.trigger("druid.window_event", window_event)
-- @usage end
-- druid.on_language_change()
---Call this function when the game language changes.
---It will notify all Druid instances to update the lang text components.
function M.on_language_change() function M.on_language_change()
local instances = get_druid_instances() events.trigger("druid.language_change")
for i = 1, #instances do
msg.post(instances[i].url, base_component.ON_LANGUAGE_CHANGE)
end
end end

37
druid/druid.script Normal file
View File

@@ -0,0 +1,37 @@
-- Place this script nearby with the gui component to able make requests
-- To the go namespace from GUI with events systems (cross context)
local event_queue = require("druid.event_queue")
---Usage: event_queue.request("druid.get_atlas_path", callback, gui.get_texture(self.node), msg.url())
---Pass texture name to get atlas info and sender url to check if the request is valid
local MESSAGE_GET_ATLAS_PATH = "druid.get_atlas_path"
---@param texture_name hash The name from gui.get_texture(node)
---@param sender hash Just msg.url from the caller
local function get_atlas_path(texture_name, sender)
local my_url = msg.url()
my_url.fragment = nil
local copy_url = msg.url(sender)
copy_url.fragment = nil
-- This check should works well
local is_my_url = my_url == copy_url
if not is_my_url then
return nil
end
return go.get(sender, "textures", { key = texture_name })
end
function init(self)
event_queue.subscribe(MESSAGE_GET_ATLAS_PATH, get_atlas_path)
end
function final(self)
event_queue.unsubscribe(MESSAGE_GET_ATLAS_PATH, get_atlas_path)
end

View File

@@ -7,7 +7,7 @@
local component = require("druid.component") local component = require("druid.component")
---@class {COMPONENT_TYPE}: druid.component ---@class {COMPONENT_TYPE}: druid.base_component
---@field druid druid_instance{COMPONENT_ANNOTATIONS} ---@field druid druid_instance{COMPONENT_ANNOTATIONS}
local M = component.create("{COMPONENT_TYPE}") local M = component.create("{COMPONENT_TYPE}")

View File

@@ -1,12 +1,9 @@
# @license MIT, Insality 2021
# @source https://github.com/Insality/druid
import os import os
import sys import sys
import deftree import deftree
current_filepath = os.path.abspath(os.path.dirname(__file__)) current_filepath = os.path.abspath(os.path.dirname(__file__))
TEMPLATE_PATH = current_filepath + "/component.lua_template" TEMPLATE_PATH = current_filepath + "/widget.lua_template"
component_annotations = "" component_annotations = ""
component_functions = "" component_functions = ""
@@ -44,13 +41,9 @@ def process_component(node_name, component_name):
component_define += "\n\tself.{0} = self.druid:new_lang_text(\"{1}\", \"lang_id\")".format(node_name, node_name) component_define += "\n\tself.{0} = self.druid:new_lang_text(\"{1}\", \"lang_id\")".format(node_name, node_name)
if node_name.startswith("grid") or node_name.startswith("static_grid"): if node_name.startswith("grid") or node_name.startswith("static_grid"):
component_annotations += "\n---@field {0} druid.static_grid".format(node_name) component_annotations += "\n---@field {0} druid.grid".format(node_name)
component_define += "\n--TODO: Replace prefab_name with grid element prefab" component_define += "\n--TODO: Replace prefab_name with grid element prefab"
component_define += "\n\tself.{0} = self.druid:new_static_grid(\"{1}\", \"prefab_name\", 1)".format(node_name, node_name) component_define += "\n\tself.{0} = self.druid:new_grid(\"{1}\", \"prefab_name\", 1)".format(node_name, node_name)
if node_name.startswith("dynamic_grid"):
component_annotations += "\n---@field {0} druid.dynamic_grid".format(node_name)
component_define += "\n\tself.{0} = self.druid:new_dynamic_grid(\"{1}\")".format(node_name, node_name)
if node_name.startswith("scroll_view"): if node_name.startswith("scroll_view"):
field_name = node_name.replace("_view", "") field_name = node_name.replace("_view", "")

View File

@@ -1,6 +1,3 @@
--- @license MIT, Insality 2021
--- @source https://github.com/Insality/druid
local M = {} local M = {}
@@ -27,18 +24,12 @@ function M.get_commands()
return { return {
{ {
label = "Assign Layers", label = "Assign Layers",
locations = { "Edit" },
locations = {"Edit"}, query = { selection = {type = "resource", cardinality = "one"} },
query = {
selection = {type = "resource", cardinality = "one"}
},
active = function(opts) active = function(opts)
local path = editor.get(opts.selection, "path") local path = editor.get(opts.selection, "path")
return ends_with(path, ".gui") return ends_with(path, ".gui")
end, end,
run = function(opts) run = function(opts)
local file = opts.selection local file = opts.selection
print("Run script for", editor.get(file, "path")) print("Run script for", editor.get(file, "path"))
@@ -59,25 +50,19 @@ function M.get_commands()
}, },
{ {
label = "Create Druid Component", label = "Create Druid Widget",
locations = { "Edit", "Assets" },
locations = {"Edit"}, query = { selection = {type = "resource", cardinality = "one"} },
query = {
selection = {type = "resource", cardinality = "one"}
},
active = function(opts) active = function(opts)
local path = editor.get(opts.selection, "path") local path = editor.get(opts.selection, "path")
return ends_with(path, ".gui") return ends_with(path, ".gui")
end, end,
run = function(opts) run = function(opts)
local file = opts.selection local file = opts.selection
print("Run script for", editor.get(file, "path")) print("Run script for", editor.get(file, "path"))
save_file_from_dependency('/druid/editor_scripts/run_python_script_on_gui.sh', "./build/run_python_script_on_gui.sh") save_file_from_dependency('/druid/editor_scripts/run_python_script_on_gui.sh', "./build/run_python_script_on_gui.sh")
save_file_from_dependency('/druid/editor_scripts/create_druid_component.py', "./build/create_druid_component.py") save_file_from_dependency('/druid/editor_scripts/create_druid_component.py', "./build/create_druid_component.py")
save_file_from_dependency('/druid/editor_scripts/component.lua_template', "./build/component.lua_template") save_file_from_dependency('/druid/editor_scripts/widget.lua_template', "./build/widget.lua_template")
return { return {
{ {
action = "shell", action = "shell",

View File

@@ -31,7 +31,7 @@ def main():
texture = node.get_attribute("texture") texture = node.get_attribute("texture")
font = node.get_attribute("font") font = node.get_attribute("font")
if texture: if texture and texture.value:
layer = texture.value.split("/")[0] layer = texture.value.split("/")[0]
node.set_attribute("layer", layer) node.set_attribute("layer", layer)

View File

@@ -0,0 +1,26 @@
---This is a template for a {COMPONENT_NAME} Druid widget.
---Instantiate this template with `druid.new_widget(widget_module, [template_id], [nodes])`.
---Read more about Druid Widgets here: ...
---@class widget.{COMPONENT_TYPE}: druid.widget
local M = {}
function M:init()
-- Now we have next functions to use here:
-- self:get_node([node_id]) -- Get node inside widget by id
-- self.druid to access Druid Instance API, like:
-- self.druid:new_button([node_id], [callback])
-- self.druid:new_text([node_id], [text])
-- And all functions from component.lua file
self.root = self:get_node("root")
self.button = self.druid:new_button("button", self.on_button, self)
end
function M:on_button()
print("Root node", self.root)
end
return M

View File

@@ -1,205 +0,0 @@
-- Copyright (c) 2021 Maksim Tuprikov <insality@gmail.com>. This code is licensed under MIT license
--- Druid Event Module
--
-- The Event module provides a simple class for handling callbacks. It is used in many Druid components.
--
-- You can subscribe to an event using the `:subscribe` method and unsubscribe using the `:unsubscribe` method.
-- @module DruidEvent
-- @alias druid.event
local M = {}
M.COUNTER = 0
-- Forward declaration
local EVENT_METATABLE
-- Local versions
local pcall = pcall
local tinsert = table.insert
local tremove = table.remove
--- DruidEvent constructor
-- @tparam function|nil callback Subscribe the callback on new event, if callback exist
-- @tparam any|nil callback_context Additional context as first param to callback call
-- @usage
-- local Event = require("druid.event")
-- ...
-- local event = Event(callback)
function M.create(callback, callback_context)
local instance = setmetatable({}, EVENT_METATABLE)
if callback then
instance:subscribe(callback, callback_context)
end
M.COUNTER = M.COUNTER + 1
return instance
end
--- Check is event subscribed.
-- @tparam DruidEvent self @{DruidEvent}
-- @tparam function callback Callback itself
-- @tparam any|nil callback_context Additional context as first param to callback call
-- @treturn boolean, number|nil @Is event subscribed, return index of callback in event as second param
function M.is_subscribed(self, callback, callback_context)
if #self == 0 then
return false, nil
end
for index = 1, #self do
local cb = self[index]
if cb[1] == callback and cb[2] == callback_context then
return true, index
end
end
return false, nil
end
--- Subscribe callback on event
-- @tparam DruidEvent self @{DruidEvent}
-- @tparam function callback Callback itself
-- @tparam any|nil callback_context Additional context as first param to callback call, usually it's self
-- @treturn boolean True if callback was subscribed
-- @usage
-- local function on_long_callback(self)
-- print("Long click!")
-- end
-- ...
-- local button = self.druid:new_button("button", callback)
-- button.on_long_click:subscribe(on_long_callback, self)
function M.subscribe(self, callback, callback_context)
assert(type(self) == "table", "You should subscribe to event with : syntax")
assert(callback, "A function must be passed to subscribe to an event")
if self:is_subscribed(callback, callback_context) then
return false
end
tinsert(self, { callback, callback_context })
return true
end
--- Unsubscribe callback on event
-- @tparam DruidEvent self @{DruidEvent}
-- @tparam function callback Callback itself
-- @tparam any|nil callback_context Additional context as first param to callback call
-- @usage
-- local function on_long_callback(self)
-- print("Long click!")
-- end
-- ...
-- button.on_long_click:unsubscribe(on_long_callback, self)
function M.unsubscribe(self, callback, callback_context)
assert(callback, "A function must be passed to subscribe to an event")
local _, event_index = self:is_subscribed(callback, callback_context)
if not event_index then
return false
end
tremove(self, event_index)
return true
end
--- Return true, if event have at lease one handler
-- @tparam DruidEvent self @{DruidEvent}
-- @treturn boolean True if event have handlers
-- @usage
-- local is_long_click_handler_exists = button.on_long_click:is_exist()
function M.is_exist(self)
return #self > 0
end
--- Return true, if event not have handler
--- @tparam DruidEvent self @{DruidEvent}
--- @treturn boolean True if event not have handlers
--- @usage
--- local is_long_click_handler_not_exists = button.on_long_click:is_empty()
function M:is_empty()
return #self == 0
end
--- Clear the all event handlers
-- @tparam DruidEvent self @{DruidEvent}
-- @usage
-- button.on_long_click:clear()
function M.clear(self)
for index = #self, 1, -1 do
self[index] = nil
end
end
--- Trigger the event and call all subscribed callbacks
-- @tparam DruidEvent self @{DruidEvent}
-- @tparam any ... All event params
-- @usage
-- local Event = require("druid.event")
-- ...
-- local event = Event()
-- event:trigger("Param1", "Param2")
function M.trigger(self, ...)
if #self == 0 then
return
end
local result = nil
local call_callback = self.call_callback
for index = 1, #self do
result = call_callback(self, self[index], ...)
end
return result
end
-- @tparam table callback Callback data {function, context}
-- @tparam any ... All event params
-- @treturn any Result of the callback
-- @local
function M:call_callback(callback, ...)
local event_callback = callback[1]
local event_callback_context = callback[2]
-- Call callback
local ok, result_or_error
if event_callback_context then
ok, result_or_error = pcall(event_callback, event_callback_context, ...)
else
ok, result_or_error = pcall(event_callback, ...)
end
-- Handle errors
if not ok then
local caller_info = debug.getinfo(2)
pprint("An error occurred during event processing", {
trigger = caller_info.short_src .. ":" .. caller_info.currentline,
error = result_or_error,
})
pprint("Traceback", debug.traceback())
return nil
end
return result_or_error
end
-- Construct event metatable
EVENT_METATABLE = {
__index = M,
__call = M.trigger,
}
return setmetatable(M, {
__call = function(_, callback)
return M.create(callback)
end,
})

84
druid/event_queue.lua Normal file
View File

@@ -0,0 +1,84 @@
local event = require("event.event")
---@class event.queue
local M = {}
local event_handlers = {}
local pending_callbacks = {}
---Request to handle a specified event and processes the queue of callbacks associated with it.
---If event has already been triggered, the callback will be executed immediately.
---If event not triggered yet, callback will be executed when event will be triggered.
---It triggered only once and then removed from the queue.
---@param event_name string The name of the event to trigger.
---@param callback fun() The callback function to execute upon triggering.
---@param ... any Additional arguments for the callback.
function M.request(event_name, callback, ...)
pending_callbacks[event_name] = pending_callbacks[event_name] or {}
table.insert(pending_callbacks[event_name], { event.create(callback), ... })
M.process_pending_callbacks(event_name)
end
---Subscribes to a specified event and executes a callback when the event is triggered.
-- If the event has already been triggered, the callback will be executed immediately.
---@param event_name string The name of the event to subscribe to.
---@param callback fun() The function to call when the event is triggered.
function M.subscribe(event_name, callback)
event_handlers[event_name] = event_handlers[event_name] or event.create()
if event_handlers[event_name] then
event_handlers[event_name]:subscribe(callback)
end
M.process_pending_callbacks(event_name)
end
---Unsubscribes a callback function from a specified event.
---@param event_name string The name of the event to unsubscribe from.
---@param callback fun() The function to remove from the event's subscription list.
function M.unsubscribe(event_name, callback)
if event_handlers[event_name] then
event_handlers[event_name]:unsubscribe(callback)
end
end
---Processes the queue for a given event name, executing callbacks and handling results.
---Processed callbacks are removed from the queue.
---@param event_name string The name of the event for which to process the queue.
function M.process_pending_callbacks(event_name)
local callbacks_to_process = pending_callbacks[event_name]
local event_handler = event_handlers[event_name]
if not callbacks_to_process or not event_handler then
return
end
-- Loop through the queue in reverse to prevent index errors during removal
for i = #callbacks_to_process, 1, -1 do
local callback_entry = callbacks_to_process[i]
-- Better to figure out how to make it without 2 unpacks, but ok for all our cases now
local args = { unpack(callback_entry, 2) }
-- Safely call the event handler and handle errors
local success, result = pcall(event_handler.trigger, event_handler, unpack(args))
if success and result then
local callback_function = callback_entry[1]
pcall(callback_function, result) -- Safely invoke the callback, catching any errors
table.remove(callbacks_to_process, i) -- Remove the processed callback from the queue
end
end
-- Clean up if the callback queue is empty
if #callbacks_to_process == 0 then
pending_callbacks[event_name] = nil
end
end
return M

View File

@@ -13,7 +13,7 @@
local const = require("druid.const") local const = require("druid.const")
local helper = require("druid.helper") local helper = require("druid.helper")
local component = require("druid.component") local component = require("druid.component")
local Event = require("druid.event") local event = require("event.event")
---@class druid.container: druid.base_component ---@class druid.container: druid.base_component
---@field node node ---@field node node
@@ -29,7 +29,7 @@ local Event = require("druid.event")
---@field fit_size vector3 ---@field fit_size vector3
---@field min_size_x number|nil ---@field min_size_x number|nil
---@field min_size_y number|nil ---@field min_size_y number|nil
---@field on_size_changed druid.event @function on_size_changed(size) ---@field on_size_changed event @function on_size_changed(size)
---@field _parent_container druid.container ---@field _parent_container druid.container
---@field _containers table ---@field _containers table
---@field _draggable_corners table ---@field _draggable_corners table
@@ -50,7 +50,7 @@ local CORNER_PIVOTS = {
--- The Container init --- The Container init
---@param node node Gui node ---@param node node Gui node
---@param mode string Layout mode ---@param mode string Layout mode
---@param callback fun(self: druid.container, size: vector3) Callback on size changed ---@param callback fun(self: druid.container, size: vector3)|nil Callback on size changed
function M:init(node, mode, callback) function M:init(node, mode, callback)
self.node = self:get_node(node) self.node = self:get_node(node)
self.druid = self:get_druid() self.druid = self:get_druid()
@@ -82,7 +82,7 @@ function M:init(node, mode, callback)
gui.set_size_mode(self.node, gui.SIZE_MODE_MANUAL) gui.set_size_mode(self.node, gui.SIZE_MODE_MANUAL)
gui.set_adjust_mode(self.node, gui.ADJUST_FIT) gui.set_adjust_mode(self.node, gui.ADJUST_FIT)
self.on_size_changed = Event(callback) self.on_size_changed = event.create(callback)
self.pivot_offset = helper.get_pivot_offset(gui.get_pivot(self.node)) self.pivot_offset = helper.get_pivot_offset(gui.get_pivot(self.node))
self.center_offset = -vmath.vector3(self.size.x * self.pivot_offset.x, self.size.y * self.pivot_offset.y, 0) self.center_offset = -vmath.vector3(self.size.x * self.pivot_offset.x, self.size.y * self.pivot_offset.y, 0)
@@ -134,8 +134,9 @@ end
--- Set new size of layout node --- Set new size of layout node
---@param width number|nil ---@param width number|nil
---@param height number|nil ---@param height number|nil
---@return druid.container @{Container} ---@param anchor_pivot constant|nil If set will keep the corner possition relative to the new size
function M:set_size(width, height) ---@return druid.container Container
function M:set_size(width, height, anchor_pivot)
width = width or self.size.x width = width or self.size.x
height = height or self.size.y height = height or self.size.y
@@ -149,11 +150,23 @@ function M:set_size(width, height)
if (width and width ~= self.size.x) or (height and height ~= self.size.y) then if (width and width ~= self.size.x) or (height and height ~= self.size.y) then
self.center_offset.x = -width * self.pivot_offset.x self.center_offset.x = -width * self.pivot_offset.x
self.center_offset.y = -height * self.pivot_offset.y self.center_offset.y = -height * self.pivot_offset.y
local dx = self.size.x - width
local dy = self.size.y - height
self.size.x = width self.size.x = width
self.size.y = height self.size.y = height
self.size.z = 0 self.size.z = 0
gui.set_size(self.node, self.size) gui.set_size(self.node, self.size)
if anchor_pivot then
local pivot = gui.get_pivot(self.node)
local pivot_offset = helper.get_pivot_offset(pivot)
local new_pivot_offset = helper.get_pivot_offset(anchor_pivot)
local position_dx = dx * (pivot_offset.x - new_pivot_offset.x)
local position_dy = dy * (pivot_offset.y - new_pivot_offset.y)
self:set_position(self._position.x + position_dx, self._position.y - position_dy)
end
self:update_child_containers() self:update_child_containers()
self.on_size_changed:trigger(self:get_context(), self.size) self.on_size_changed:trigger(self:get_context(), self.size)
end end
@@ -162,6 +175,11 @@ function M:set_size(width, height)
end end
function M:get_position()
return self._position
end
---@param pos_x number ---@param pos_x number
---@param pos_y number ---@param pos_y number
function M:set_position(pos_x, pos_y) function M:set_position(pos_x, pos_y)
@@ -178,7 +196,7 @@ end
---Get current size of layout node ---Get current size of layout node
---@return vector3 size ---@return vector3 size
function M:get_size() function M:get_size()
return self.size return vmath.vector3(self.size)
end end
@@ -191,22 +209,22 @@ end
--- Set size for layout node to fit inside it --- Set size for layout node to fit inside it
---@param target_size vector3 ---@param target_size vector3
---@return druid.container @{Container} ---@return druid.container Container
function M:fit_into_size(target_size) function M:fit_into_size(target_size)
self.fit_size = target_size self.fit_size = target_size
self:refresh() self:refresh()
return self return self
end end
--- Set current size for layout node to fit inside it --- Set current size for layout node to fit inside it
---@return druid.container @{Container} ---@return druid.container Container
function M:fit_into_window() function M:fit_into_window()
return self:fit_into_size(vmath.vector3(gui.get_width(), gui.get_height(), 0)) return self:fit_into_size(vmath.vector3(gui.get_width(), gui.get_height(), 0))
end end
---@param self druid.container
function M:on_window_resized() function M:on_window_resized()
local x_koef, y_koef = helper.get_screen_aspect_koef() local x_koef, y_koef = helper.get_screen_aspect_koef()
self.x_koef = x_koef self.x_koef = x_koef
@@ -221,7 +239,7 @@ end
---@param node_or_container node|string|druid.container|table ---@param node_or_container node|string|druid.container|table
---@param mode string|nil stretch, fit, stretch_x, stretch_y. Default: Pick from node, "fit" or "stretch" ---@param mode string|nil stretch, fit, stretch_x, stretch_y. Default: Pick from node, "fit" or "stretch"
---@param on_resize_callback fun(self: userdata, size: vector3)|nil ---@param on_resize_callback fun(self: userdata, size: vector3)|nil
---@return druid.container @{Container} New created layout instance ---@return druid.container Container New created layout instance
function M:add_container(node_or_container, mode, on_resize_callback) function M:add_container(node_or_container, mode, on_resize_callback)
local container = nil local container = nil
local node = node_or_container local node = node_or_container
@@ -422,7 +440,7 @@ function M:update_child_containers()
end end
---@return druid.container @{Container} ---@return druid.container Container
function M:create_draggable_corners() function M:create_draggable_corners()
self:clear_draggable_corners() self:clear_draggable_corners()
@@ -452,7 +470,7 @@ function M:create_draggable_corners()
end end
---@return druid.container @{Container} ---@return druid.container Container
function M:clear_draggable_corners() function M:clear_draggable_corners()
for index = 1, #self._draggable_corners do for index = 1, #self._draggable_corners do
local drag_component = self._draggable_corners[index] local drag_component = self._draggable_corners[index]
@@ -505,7 +523,7 @@ end
--- Set node for layout node to fit inside it. Pass nil to reset --- Set node for layout node to fit inside it. Pass nil to reset
---@param node string|node The node_id or gui.get_node(node_id) ---@param node string|node The node_id or gui.get_node(node_id)
---@return druid.container @{Layout} ---@return druid.container Layout
function M:fit_into_node(node) function M:fit_into_node(node)
self._fit_node = self:get_node(node) self._fit_node = self:get_node(node)
self:refresh_scale() self:refresh_scale()

View File

@@ -10,10 +10,10 @@
--- The Druid scroll component --- The Druid scroll component
-- @tfield Scroll scroll @{Scroll} -- @tfield Scroll scroll Scroll
--- The Druid Grid component --- The Druid Grid component
-- @tfield StaticGrid grid @{StaticGrid}, @{DynamicGrid} -- @tfield StaticGrid grid StaticGrid}, @{DynamicGrid
--- The current progress of scroll posititon --- The current progress of scroll posititon
-- @tfield number scroll_progress -- @tfield number scroll_progress
@@ -25,30 +25,41 @@
-- @tfield number last_index -- @tfield number last_index
--- Event triggered when scroll progress is changed; event(self, progress_value) --- Event triggered when scroll progress is changed; event(self, progress_value)
-- @tfield DruidEvent on_scroll_progress_change @{DruidEvent} -- @tfield event on_scroll_progress_change event
---On DataList visual element created Event callback(self, index, node, instance) ---On DataList visual element created Event callback(self, index, node, instance)
-- @tfield DruidEvent on_element_add @{DruidEvent} -- @tfield event on_element_add event
---On DataList visual element created Event callback(self, index) ---On DataList visual element created Event callback(self, index)
-- @tfield DruidEvent on_element_remove @{DruidEvent} -- @tfield event on_element_remove event
--- ---
local const = require("druid.const") local const = require("druid.const")
local helper = require("druid.helper") local helper = require("druid.helper")
local component = require("druid.component") local component = require("druid.component")
local Event = require("druid.event") local event = require("event.event")
local DataList = component.create("data_list") ---@class druid.data_list: druid.base_component
---@field scroll druid.scroll
---@field grid druid.grid
---@field on_scroll_progress_change event
---@field on_element_add event
---@field on_element_remove event
---@field private _create_function function
---@field private _is_use_cache boolean
---@field private _cache table
---@field private _data table
---@field private _data_visual table
---@field top_index number
local M = component.create("data_list")
--- The @{DataList} constructor --- The DataList constructor
-- @tparam DataList self @{DataList} ---@param scroll druid.scroll The Scroll instance for Data List component
-- @tparam Scroll scroll The @{Scroll} instance for Data List component ---@param grid druid.grid The StaticGrid} or @{DynamicGrid instance for Data List component
-- @tparam StaticGrid grid The @{StaticGrid} or @{DynamicGrid} instance for Data List component ---@param create_function function The create function callback(self, data, index, data_list). Function should return (node, [component])
-- @tparam function create_function The create function callback(self, data, index, data_list). Function should return (node, [component]) function M:init(scroll, grid, create_function)
function DataList.init(self, scroll, grid, create_function)
self.scroll = scroll self.scroll = scroll
self.grid = grid self.grid = grid
if self.grid.style then if self.grid.style then
@@ -68,35 +79,32 @@ function DataList.init(self, scroll, grid, create_function)
self.scroll.on_scroll:subscribe(self._refresh, self) self.scroll.on_scroll:subscribe(self._refresh, self)
self.on_scroll_progress_change = Event() self.on_scroll_progress_change = event.create()
self.on_element_add = Event() self.on_element_add = event.create()
self.on_element_remove = Event() self.on_element_remove = event.create()
end end
--- Druid System on_remove function --- Druid System on_remove function
-- @tparam DataList self @{DataList} function M:on_remove()
function DataList.on_remove(self)
self:clear() self:clear()
self.scroll.on_scroll:unsubscribe(self._refresh, self) self.scroll.on_scroll:unsubscribe(self._refresh, self)
end end
--- Set refresh function for DataList component --- Set refresh function for DataList component
-- @tparam DataList self @{DataList} ---@param is_use_cache boolean Use cache version of DataList. Requires make setup of components in on_element_add callback and clean in on_element_remove
-- @tparam boolean is_use_cache Use cache version of DataList. Requires make setup of components in on_element_add callback and clean in on_element_remove ---@return druid.data_list Current DataList instance
-- @treturn druid.data_list Current DataList instance function M:set_use_cache(is_use_cache)
function DataList.set_use_cache(self, is_use_cache)
self._is_use_cache = is_use_cache self._is_use_cache = is_use_cache
return self return self
end end
--- Set new data set for DataList component --- Set new data set for DataList component
-- @tparam DataList self @{DataList} ---@param data table The new data array
-- @tparam table data The new data array ---@return druid.data_list Current DataList instance
-- @treturn druid.data_list Current DataList instance function M:set_data(data)
function DataList.set_data(self, data)
self._data = data or {} self._data = data or {}
self:_refresh() self:_refresh()
@@ -105,19 +113,17 @@ end
--- Return current data from DataList component --- Return current data from DataList component
-- @tparam DataList self @{DataList} ---@return table The current data array
-- @treturn table The current data array function M:get_data()
function DataList.get_data(self)
return self._data return self._data
end end
--- Add element to DataList. Currenly untested --- Add element to DataList. Currenly untested
-- @tparam DataList self @{DataList} ---@param data table
-- @tparam table data ---@param index number|nil
-- @tparam number|nil index ---@param shift_policy number|nil The constant from const.SHIFT.*
-- @tparam number|nil shift_policy The constant from const.SHIFT.* function M:add(data, index, shift_policy)
function DataList.add(self, data, index, shift_policy)
index = index or #self._data + 1 index = index or #self._data + 1
shift_policy = shift_policy or const.SHIFT.RIGHT shift_policy = shift_policy or const.SHIFT.RIGHT
@@ -127,20 +133,18 @@ end
--- Remove element from DataList. Currenly untested --- Remove element from DataList. Currenly untested
-- @tparam DataList self @{DataList} ---@param index number|nil
-- @tparam number|nil index ---@param shift_policy number|nil The constant from const.SHIFT.*
-- @tparam number|nil shift_policy The constant from const.SHIFT.* function M:remove(index, shift_policy)
function DataList.remove(self, index, shift_policy)
helper.remove_with_shift(self._data, index, shift_policy) helper.remove_with_shift(self._data, index, shift_policy)
self:_refresh() self:_refresh()
end end
--- Remove element from DataList by data value. Currenly untested --- Remove element from DataList by data value. Currenly untested
-- @tparam DataList self @{DataList} ---@param data table
-- @tparam table data ---@param shift_policy number|nil The constant from const.SHIFT.*
-- @tparam number|nil shift_policy The constant from const.SHIFT.* function M:remove_by_data(data, shift_policy)
function DataList.remove_by_data(self, data, shift_policy)
local index = helper.contains(self._data, data) local index = helper.contains(self._data, data)
if index then if index then
helper.remove_with_shift(self._data, index, shift_policy) helper.remove_with_shift(self._data, index, shift_policy)
@@ -150,17 +154,15 @@ end
--- Clear the DataList and refresh visuals --- Clear the DataList and refresh visuals
-- @tparam DataList self @{DataList} function M:clear()
function DataList.clear(self)
self._data = {} self._data = {}
self:_refresh() self:_refresh()
end end
--- Return index for data value --- Return index for data value
-- @tparam DataList self @{DataList} ---@param data table
-- @tparam table data function M:get_index(data)
function DataList.get_index(self, data)
for index, value in pairs(self._data) do for index, value in pairs(self._data) do
if value == data then if value == data then
return index return index
@@ -172,9 +174,8 @@ end
--- Return all currenly created nodes in DataList --- Return all currenly created nodes in DataList
-- @tparam DataList self @{DataList} ---@return node[] List of created nodes
-- @treturn node[] List of created nodes function M:get_created_nodes()
function DataList.get_created_nodes(self)
local nodes = {} local nodes = {}
for index, data in pairs(self._data_visual) do for index, data in pairs(self._data_visual) do
@@ -186,9 +187,8 @@ end
--- Return all currenly created components in DataList --- Return all currenly created components in DataList
-- @tparam DataList self @{DataList} ---@return druid.base_component[] List of created nodes
-- @treturn druid.base_component[] List of created nodes function M:get_created_components()
function DataList.get_created_components(self)
local components = {} local components = {}
for index, data in pairs(self._data_visual) do for index, data in pairs(self._data_visual) do
@@ -200,19 +200,17 @@ end
--- Instant scroll to element with passed index --- Instant scroll to element with passed index
-- @tparam DataList self @{DataList} ---@param index number
-- @tparam number index function M:scroll_to_index(index)
function DataList.scroll_to_index(self, index)
local pos = self.grid:get_pos(index) local pos = self.grid:get_pos(index)
self.scroll:scroll_to(pos) self.scroll:scroll_to(pos)
end end
--- Add element at passed index using cache or create new --- Add element at passed index using cache or create new
-- @tparam DataList self @{DataList} ---@param index number
-- @tparam number index ---@private
-- @local function M:_add_at(index)
function DataList._add_at(self, index)
if self._data_visual[index] then if self._data_visual[index] then
self:_remove_at(index) self:_remove_at(index)
end end
@@ -243,10 +241,9 @@ end
--- Remove element from passed index and add it to cache if applicable --- Remove element from passed index and add it to cache if applicable
-- @tparam DataList self @{DataList} ---@param index number
-- @tparam number index ---@private
-- @local function M:_remove_at(index)
function DataList._remove_at(self, index)
self.grid:remove(index, const.SHIFT.NO_SHIFT) self.grid:remove(index, const.SHIFT.NO_SHIFT)
local visual_data = self._data_visual[index] local visual_data = self._data_visual[index]
@@ -274,9 +271,8 @@ end
--- Refresh all elements in DataList --- Refresh all elements in DataList
-- @tparam DataList self @{DataList} ---@private
-- @local function M:_refresh()
function DataList._refresh(self)
self.scroll:set_size(self.grid:get_size_for(#self._data)) self.scroll:set_size(self.grid:get_size_for(#self._data))
local start_pos = -self.scroll.position --[[@as vector3]] local start_pos = -self.scroll.position --[[@as vector3]]
@@ -313,4 +309,4 @@ function DataList._refresh(self)
end end
return DataList return M

View File

@@ -1,427 +0,0 @@
-- Copyright (c) 2021 Maksim Tuprikov <insality@gmail.com>. This code is licensed under MIT license
--- Component to handle placing components in row
--
-- <a href="https://insality.github.io/druid/druid/index.html?example=general_grid" target="_blank"><b>Example Link</b></a>
-- @module DynamicGrid
-- @within BaseComponent
-- @alias druid.dynamic_grid
--- On item add callback(self, node, index)
-- @tfield DruidEvent on_add_item @{DruidEvent}
--- On item remove callback(self, index)
-- @tfield DruidEvent on_remove_item @{DruidEvent}
--- On item add or remove callback(self, index)
-- @tfield DruidEvent on_change_items @{DruidEvent}
--- On grid clear callback(self)
-- @tfield DruidEvent on_clear @{DruidEvent}
--- On update item positions callback(self)
-- @tfield DruidEvent on_update_positions @{DruidEvent}
--- Parent gui node
-- @tfield node parent
--- List of all grid elements. Contains from node, pos, size, pivot
-- @tfield node[] nodes
--- The first index of node in grid
-- @tfield number first_index
--- The last index of node in grid
-- @tfield number last_index
--- Item size
-- @tfield vector3 node_size
--- The size of item content
-- @tfield vector4 border
---
local const = require("druid.const")
local Event = require("druid.event")
local helper = require("druid.helper")
local component = require("druid.component")
local DynamicGrid = component.create("dynamic_grid")
local SIDE_VECTORS = {
LEFT = vmath.vector3(-1, 0, 0),
RIGHT = vmath.vector3(1, 0, 0),
TOP = vmath.vector3(0, -1, 0),
BOT = vmath.vector3(0, 1, 0),
}
local AVAILABLE_PIVOTS = {
gui.PIVOT_N,
gui.PIVOT_S,
gui.PIVOT_W,
gui.PIVOT_E,
}
--- The @{DynamicGrid} constructor
-- @tparam DynamicGrid self @{DynamicGrid}
-- @tparam node parent The gui node parent, where items will be placed
function DynamicGrid.init(self, parent)
self.parent = self:get_node(parent)
local parent_pivot = gui.get_pivot(self.parent)
self.pivot = helper.get_pivot_offset(parent_pivot)
assert(helper.contains(AVAILABLE_PIVOTS, parent_pivot), const.ERRORS.GRID_DYNAMIC_ANCHOR)
self.side = ((parent_pivot == gui.PIVOT_W or parent_pivot == gui.PIVOT_E)
and const.SIDE.X or const.SIDE.Y)
self.nodes = {}
self.border = vmath.vector4(0) -- Current grid content size
self.on_add_item = Event()
self.on_remove_item = Event()
self.on_change_items = Event()
self.on_clear = Event()
self.on_update_positions = Event()
self._set_position_function = gui.set_position
end
function DynamicGrid.on_layout_change(self)
self:_update(true)
end
--- Return pos for grid node index
-- @tparam DynamicGrid self @{DynamicGrid}
-- @tparam number index The grid element index
-- @tparam node node The node to be placed
-- @tparam number|nil origin_index Index of nearby node
-- @treturn vector3 node position
function DynamicGrid.get_pos(self, index, node, origin_index)
local origin_node = self.nodes[origin_index]
-- If anchor node is not exist, check around nodes
if not origin_node then
if self.nodes[index + 1] then
origin_index = index + 1
end
if self.nodes[index - 1] then
origin_index = index - 1
end
origin_node = self.nodes[origin_index]
end
if not origin_node then
assert(not self.first_index, "Dynamic Grid can't have gaps between nodes. Error on grid:add")
-- If not origin node, so it should be first element in the grid
local size = helper.get_scaled_size(node)
local pivot = const.PIVOTS[gui.get_pivot(node)]
return vmath.vector3(
size.x * pivot.x - size.x * self.pivot.x,
size.y * pivot.y - size.y * self.pivot.y,
0)
end
if origin_node then
-- Other nodes spawn from other side of the origin node
local is_forward = origin_index < index
local delta = is_forward and 1 or -1
return self:_get_next_node_pos(index - delta, node, self:_get_side_vector(self.side, is_forward))
end
end
--- Add new node to the grid
-- @tparam DynamicGrid self @{DynamicGrid}
-- @tparam node node Gui node
-- @tparam number|nil index The node position. By default add as last node
-- @tparam number|nil shift_policy How shift nodes, if required. Default: const.SHIFT.RIGHT
-- @tparam boolean|nil is_instant If true, update node positions instantly
function DynamicGrid.add(self, node, index, shift_policy, is_instant)
shift_policy = shift_policy or const.SHIFT.RIGHT
local delta = shift_policy -- -1 or 1 or 0
-- By default add node at end
index = index or ((self.last_index or 0) + 1)
-- If node exist at index place, shifting them
local is_shift = self.nodes[index] and shift_policy ~= const.SHIFT.NO_SHIFT
if is_shift then
-- We need to iterate from index to start or end grid, depends of shift side
local start_index = shift_policy == const.SHIFT.LEFT and self.first_index or self.last_index
for i = start_index, index, -delta do
self.nodes[i + delta] = self.nodes[i]
end
end
self:_add_node(node, index, index - delta)
-- After shifting we should recalc node poses
if is_shift then
-- We need to iterate from placed node to start or end grid, depends of shift side
local target_index = shift_policy == const.SHIFT.LEFT and self.first_index or self.last_index
for i = index + delta, target_index + delta, delta do
local move_node = self.nodes[i]
move_node.pos = self:get_pos(i, move_node.node, i - delta)
end
end
-- Sync grid data
self:_update(is_instant)
self.on_add_item:trigger(self:get_context(), node, index)
self.on_change_items:trigger(self:get_context(), index)
end
--- Remove the item from the grid. Note that gui node will be not deleted
-- @tparam DynamicGrid self @{DynamicGrid}
-- @tparam number index The grid node index to remove
-- @tparam number|nil shift_policy How shift nodes, if required. Default: const.SHIFT.RIGHT
-- @tparam boolean|nil is_instant If true, update node positions instantly
-- @treturn node The deleted gui node from grid
function DynamicGrid.remove(self, index, shift_policy, is_instant)
shift_policy = shift_policy or const.SHIFT.RIGHT
local delta = shift_policy -- -1 or 1 or 0
assert(self.nodes[index], "No grid item at given index " .. index)
-- Just set nil for delete node data
local removed_node = self.nodes[index].node
self.nodes[index] = nil
-- After delete node, we should shift nodes and recalc their poses, depends from is_shift_left
if shift_policy ~= const.SHIFT.NO_SHIFT then
local target_index = shift_policy == const.SHIFT.LEFT and self.first_index or self.last_index
for i = index, target_index, delta do
self.nodes[i] = self.nodes[i + delta]
if self.nodes[i] then
self.nodes[i].pos = self:get_pos(i, self.nodes[i].node, i - delta)
end
end
end
-- Sync grid data
self:_update(is_instant)
self.on_remove_item:trigger(self:get_context(), index)
self.on_change_items:trigger(self:get_context(), index)
return removed_node
end
--- Return grid content size
-- @tparam DynamicGrid self @{DynamicGrid}
-- @tparam vector3 border
-- @treturn vector3 The grid content size
function DynamicGrid.get_size(self, border)
border = border or self.border
return vmath.vector3(
border.z - border.x,
border.y - border.w,
0)
end
--- Return DynamicGrid offset, where DynamicGrid content starts.
-- @tparam DynamicGrid self @{DynamicGrid} The DynamicGrid instance
-- @treturn vector3 The DynamicGrid offset
function DynamicGrid.get_offset(self)
local size = self:get_size()
local borders = self:get_borders()
local offset = vmath.vector3(
(borders.z + borders.x)/2 + size.x * self.pivot.x,
(borders.y + borders.w)/2 + size.y * self.pivot.y,
0)
return offset
end
--- Return grid content borders
-- @tparam DynamicGrid self @{DynamicGrid}
-- @treturn vector3 The grid content borders
function DynamicGrid.get_borders(self)
return self.border
end
--- Return grid index by node
-- @tparam DynamicGrid self @{DynamicGrid}
-- @tparam node node The gui node in the grid
-- @treturn number The node index
function DynamicGrid.get_index_by_node(self, node)
for index, node_info in pairs(self.nodes) do
if node == node_info.node then
return index
end
end
return nil
end
--- Return array of all node positions
-- @tparam DynamicGrid self @{DynamicGrid}
-- @treturn vector3[] All grid node positions
function DynamicGrid.get_all_pos(self)
local result = {}
for i, node in pairs(self.nodes) do
table.insert(result, gui.get_position(node.node))
end
return result
end
--- Change set position function for grid nodes. It will call on
-- update poses on grid elements. Default: gui.set_position
-- @tparam DynamicGrid self @{DynamicGrid}
-- @tparam function callback Function on node set position
-- @treturn druid.dynamic_grid Current grid instance
function DynamicGrid.set_position_function(self, callback)
self._set_position_function = callback or gui.set_position
return self
end
--- Clear grid nodes array. GUI nodes will be not deleted!
-- If you want to delete GUI nodes, use dynamic_grid.nodes array before grid:clear
-- @tparam DynamicGrid self @{DynamicGrid}
-- @treturn druid.dynamic_grid Current grid instance
function DynamicGrid.clear(self)
self.nodes = {}
self:_update()
self.on_clear:trigger(self:get_context())
return self
end
function DynamicGrid._add_node(self, node, index, origin_index)
self.nodes[index] = {
node = node,
pos = self:get_pos(index, node, origin_index),
size = helper.get_scaled_size(node),
pivot = const.PIVOTS[gui.get_pivot(node)]
}
-- Add new item instantly in new pos
gui.set_parent(node, self.parent)
gui.set_position(node, self.nodes[index].pos)
end
--- Update grid inner state
-- @tparam DynamicGrid self @{DynamicGrid}
-- @tparam boolean|nil is_instant If true, node position update instantly, otherwise with set_position_function callback
-- @local
function DynamicGrid._update(self, is_instant)
self:_update_indexes()
self:_update_borders()
self:_update_pos(is_instant)
end
--- Update first and last indexes of grid nodes
-- @tparam DynamicGrid self @{DynamicGrid}
-- @local
function DynamicGrid._update_indexes(self)
self.first_index = nil
self.last_index = nil
for index in pairs(self.nodes) do
self.first_index = self.first_index or index
self.last_index = self.last_index or index
self.first_index = math.min(self.first_index, index)
self.last_index = math.max(self.last_index, index)
end
end
--- Update grid content borders, recalculate min and max values
-- @tparam DynamicGrid self @{DynamicGrid}
-- @local
function DynamicGrid._update_borders(self)
if not self.first_index then
self.border = vmath.vector4(0)
return
end
self.border = vmath.vector4(math.huge, -math.huge, -math.huge, math.huge)
for index, node in pairs(self.nodes) do
local pos = node.pos
local size = node.size
local pivot = node.pivot
local left = pos.x - size.x/2 - (size.x * pivot.x)
local right = pos.x + size.x/2 - (size.x * pivot.x)
local top = pos.y + size.y/2 - (size.y * pivot.y)
local bottom = pos.y - size.y/2 - (size.y * pivot.y)
self.border.x = math.min(self.border.x, left)
self.border.y = math.max(self.border.y, top)
self.border.z = math.max(self.border.z, right)
self.border.w = math.min(self.border.w, bottom)
end
end
--- Update grid nodes position
-- @tparam DynamicGrid self @{DynamicGrid}
-- @tparam boolean|nil is_instant If true, node position update instantly, otherwise with set_position_function callback
-- @local
function DynamicGrid._update_pos(self, is_instant)
for index, node in pairs(self.nodes) do
if is_instant then
gui.set_position(node.node, node.pos)
else
self._set_position_function(node.node, node.pos)
end
end
self.on_update_positions:trigger(self:get_context())
end
function DynamicGrid._get_next_node_pos(self, origin_node_index, new_node, place_side)
local node = self.nodes[origin_node_index]
local new_node_size = helper.get_scaled_size(new_node)
local new_pivot = const.PIVOTS[gui.get_pivot(new_node)]
local dist_x = (node.size.x/2 + new_node_size.x/2) * place_side.x
local dist_y = (node.size.y/2 + new_node_size.y/2) * place_side.y
local node_center_x = node.pos.x - node.size.x * node.pivot.x
local node_center_y = node.pos.y - node.size.y * node.pivot.y
return vmath.vector3(
node_center_x + dist_x + new_node_size.x * new_pivot.x,
node_center_y - dist_y + new_node_size.y * new_pivot.y,
0
)
end
--- Return side vector to correct node shifting
function DynamicGrid._get_side_vector(self, side, is_forward)
if side == const.SIDE.X then
return is_forward and SIDE_VECTORS.RIGHT or SIDE_VECTORS.LEFT
end
if side == const.SIDE.Y then
return is_forward and SIDE_VECTORS.BOT or SIDE_VECTORS.TOP
end
end
return DynamicGrid

View File

@@ -8,10 +8,10 @@
-- @alias druid.hotkey -- @alias druid.hotkey
--- On hotkey released callback(self, argument) --- On hotkey released callback(self, argument)
-- @tfield DruidEvent on_hotkey_pressed @{DruidEvent} -- @tfield event on_hotkey_pressed event
--- On hotkey released callback(self, argument) --- On hotkey released callback(self, argument)
-- @tfield DruidEvent on_hotkey_released @{DruidEvent} -- @tfield event on_hotkey_released event
--- Visual node --- Visual node
-- @tfield node node -- @tfield node node
@@ -20,30 +20,35 @@
-- @tfield node|nil click_node -- @tfield node|nil click_node
--- Button component from click_node --- Button component from click_node
-- @tfield Button button @{Button} -- @tfield Button button Button
--- ---
local helper = require("druid.helper") local helper = require("druid.helper")
local component = require("druid.component") local component = require("druid.component")
local Event = require("druid.event") local event = require("event.event")
local Hotkey = component.create("hotkey") ---@class druid.hotkey: druid.base_component
---@field on_hotkey_pressed event
---@field on_hotkey_released event
---@field style table
---@field private _hotkeys table
---@field private _modificators table
local M = component.create("hotkey")
--- The @{Hotkey} constructor --- The Hotkey constructor
-- @tparam Hotkey self @{Hotkey} ---@param keys string[]|string The keys to be pressed for trigger callback. Should contains one key and any modificator keys
-- @tparam string[]|string keys The keys to be pressed for trigger callback. Should contains one key and any modificator keys ---@param callback function The callback function
-- @tparam function callback The callback function ---@param callback_argument any|nil The argument to pass into the callback function
-- @tparam any|nil callback_argument The argument to pass into the callback function function M:init(keys, callback, callback_argument)
function Hotkey.init(self, keys, callback, callback_argument)
self.druid = self:get_druid() self.druid = self:get_druid()
self._hotkeys = {} self._hotkeys = {}
self._modificators = {} self._modificators = {}
self.on_hotkey_pressed = Event() self.on_hotkey_pressed = event.create()
self.on_hotkey_released = Event(callback) self.on_hotkey_released = event.create(callback)
if keys then if keys then
self:add_hotkey(keys, callback_argument) self:add_hotkey(keys, callback_argument)
@@ -56,7 +61,7 @@ end
-- or create your own style -- or create your own style
-- @table style -- @table style
-- @tfield string[] MODIFICATORS The list of action_id as hotkey modificators -- @tfield string[] MODIFICATORS The list of action_id as hotkey modificators
function Hotkey.on_style_change(self, style) function M:on_style_change(style)
self.style = {} self.style = {}
self.style.MODIFICATORS = style.MODIFICATORS or {} self.style.MODIFICATORS = style.MODIFICATORS or {}
@@ -67,11 +72,10 @@ end
--- Add hotkey for component callback --- Add hotkey for component callback
-- @tparam Hotkey self @{Hotkey} ---@param keys string[]|hash[]|string|hash that have to be pressed before key pressed to activate
-- @tparam string[]|hash[]|string|hash keys that have to be pressed before key pressed to activate ---@param callback_argument any|nil The argument to pass into the callback function
-- @tparam any|nil callback_argument The argument to pass into the callback function ---@return druid.hotkey Current instance
-- @treturn Hotkey Current instance function M:add_hotkey(keys, callback_argument)
function Hotkey.add_hotkey(self, keys, callback_argument)
keys = keys or {} keys = keys or {}
if type(keys) == "string" then if type(keys) == "string" then
keys = { keys } keys = { keys }
@@ -82,7 +86,7 @@ function Hotkey.add_hotkey(self, keys, callback_argument)
for index = 1, #keys do for index = 1, #keys do
local key_hash = hash(keys[index]) local key_hash = hash(keys[index])
if helper.contains(self.style.MODIFICATORS, key_hash) then if #keys > 1 and helper.contains(self.style.MODIFICATORS, key_hash) then
table.insert(modificators, key_hash) table.insert(modificators, key_hash)
else else
if not key then if not key then
@@ -110,33 +114,43 @@ function Hotkey.add_hotkey(self, keys, callback_argument)
end end
function Hotkey.on_focus_gained(self) function M:is_processing()
for index = 1, #self._hotkeys do
if self._hotkeys[index].is_processing then
return true
end
end
return false
end
function M:on_focus_gained()
for k, v in pairs(self._modificators) do for k, v in pairs(self._modificators) do
self._modificators[k] = false self._modificators[k] = false
end end
end end
function Hotkey.on_input(self, action_id, action) function M:on_input(action_id, action)
if not action_id or #self._hotkeys == 0 then if not action_id then
return false return false
end end
if self._modificators[action_id] ~= nil then if self._modificators[action_id] ~= nil and action.pressed then
if action.pressed then
self._modificators[action_id] = true self._modificators[action_id] = true
end end
if action.released then
self._modificators[action_id] = false
end
end
for index = 1, #self._hotkeys do for index = 1, #self._hotkeys do
local hotkey = self._hotkeys[index] local hotkey = self._hotkeys[index]
if action_id == hotkey.key then local is_relative_key = helper.contains(self.style.MODIFICATORS, action_id) or action_id == hotkey.key
if is_relative_key and (action_id == hotkey.key or not hotkey.key) then
local is_modificator_ok = true local is_modificator_ok = true
local is_consume = not not (hotkey.key)
-- Check only required modificators pressed -- Check only required modificators pressed
if hotkey.key and #hotkey.modificators > 0 then
for i = 1, #self.style.MODIFICATORS do for i = 1, #self.style.MODIFICATORS do
local mod = self.style.MODIFICATORS[i] local mod = self.style.MODIFICATORS[i]
if helper.contains(hotkey.modificators, mod) and self._modificators[mod] == false then if helper.contains(hotkey.modificators, mod) and self._modificators[mod] == false then
@@ -146,6 +160,7 @@ function Hotkey.on_input(self, action_id, action)
is_modificator_ok = false is_modificator_ok = false
end end
end end
end
if action.pressed and is_modificator_ok then if action.pressed and is_modificator_ok then
hotkey.is_processing = true hotkey.is_processing = true
@@ -153,28 +168,31 @@ function Hotkey.on_input(self, action_id, action)
end end
if not action.pressed and self._is_process_repeated and action.repeated and is_modificator_ok and hotkey.is_processing then if not action.pressed and self._is_process_repeated and action.repeated and is_modificator_ok and hotkey.is_processing then
self.on_hotkey_released:trigger(self:get_context(), hotkey.callback_argument) self.on_hotkey_released:trigger(self:get_context(), hotkey.callback_argument)
return true return is_consume
end end
if action.released and is_modificator_ok and hotkey.is_processing then if action.released and is_modificator_ok and hotkey.is_processing then
hotkey.is_processing = false
self.on_hotkey_released:trigger(self:get_context(), hotkey.callback_argument) self.on_hotkey_released:trigger(self:get_context(), hotkey.callback_argument)
return true hotkey.is_processing = false
return is_consume
end end
end end
end end
if self._modificators[action_id] ~= nil and action.released then
self._modificators[action_id] = false
end
return false return false
end end
--- If true, the callback will be triggered on action.repeated --- If true, the callback will be triggered on action.repeated
-- @tparam Hotkey self @{Hotkey} ---@param is_enabled_repeated bool The flag value
-- @tparam bool is_enabled_repeated The flag value ---@return druid.hotkey
-- @treturn Hotkey function M:set_repeat(is_enabled_repeated)
function Hotkey.set_repeat(self, is_enabled_repeated)
self._is_process_repeated = is_enabled_repeated self._is_process_repeated = is_enabled_repeated
return self return self
end end
return Hotkey return M

View File

@@ -10,25 +10,25 @@
-- @alias druid.input -- @alias druid.input
--- On input field select callback(self, input_instance) --- On input field select callback(self, input_instance)
-- @tfield DruidEvent on_input_select @{DruidEvent} -- @tfield event on_input_select event
--- On input field unselect callback(self, input_text, input_instance) --- On input field unselect callback(self, input_text, input_instance)
-- @tfield DruidEvent on_input_unselect @{DruidEvent} -- @tfield event on_input_unselect event
--- On input field text change callback(self, input_text) --- On input field text change callback(self, input_text)
-- @tfield DruidEvent on_input_text @{DruidEvent} -- @tfield event on_input_text event
--- On input field text change to empty string callback(self, input_text) --- On input field text change to empty string callback(self, input_text)
-- @tfield DruidEvent on_input_empty @{DruidEvent} -- @tfield event on_input_empty event
--- On input field text change to max length string callback(self, input_text) --- On input field text change to max length string callback(self, input_text)
-- @tfield DruidEvent on_input_full @{DruidEvent} -- @tfield event on_input_full event
--- On trying user input with not allowed character callback(self, params, input_text) --- On trying user input with not allowed character callback(self, params, input_text)
-- @tfield DruidEvent on_input_wrong @{DruidEvent} -- @tfield event on_input_wrong event
--- On cursor position change callback(self, cursor_index, start_index, end_index) --- On cursor position change callback(self, cursor_index, start_index, end_index)
-- @tfield DruidEvent on_select_cursor_change @{DruidEvent} -- @tfield event on_select_cursor_change event
--- The cursor index. The index of letter cursor after. Leftmost cursor - 0 --- The cursor index. The index of letter cursor after. Leftmost cursor - 0
-- @tfield number cursor_index -- @tfield number cursor_index
@@ -40,7 +40,7 @@
-- @tfield number end_index -- @tfield number end_index
--- Text component --- Text component
-- @tfield Text text @{Text} -- @tfield Text text Text
--- Current input value --- Current input value
-- @tfield string value -- @tfield string value
@@ -61,7 +61,7 @@
-- @tfield number marked_text_width -- @tfield number marked_text_width
--- Button component --- Button component
-- @tfield Button button @{Button} -- @tfield Button button Button
--- Is current input selected now --- Is current input selected now
-- @tfield boolean is_selected -- @tfield boolean is_selected
@@ -80,16 +80,26 @@
--- ---
local Event = require("druid.event") local event = require("event.event")
local const = require("druid.const") local const = require("druid.const")
local helper = require("druid.helper") local helper = require("druid.helper")
local component = require("druid.component") local component = require("druid.component")
local utf8_lua = require("druid.system.utf8") local utf8_lua = require("druid.system.utf8")
local utf8 = utf8 or utf8_lua local utf8 = utf8 or utf8_lua
local Input = component.create("input") ---@class druid.input: druid.base_component
---@field on_input_select event
---@field on_input_unselect event
---@field on_input_text event
---@field on_input_empty event
---@field on_input_full event
---@field on_input_wrong event
---@field on_select_cursor_change event
---@field style table
---@field text druid.text
local M = component.create("input")
Input.ALLOWED_ACTIONS = { M.ALLOWED_ACTIONS = {
[const.ACTION_TOUCH] = true, [const.ACTION_TOUCH] = true,
[const.ACTION_TEXT] = true, [const.ACTION_TEXT] = true,
[const.ACTION_MARKED_TEXT] = true, [const.ACTION_MARKED_TEXT] = true,
@@ -99,9 +109,9 @@ Input.ALLOWED_ACTIONS = {
} }
--- Mask text by replacing every character with a mask character --- Mask text by replacing every character with a mask character
-- @tparam string text ---@param text string
-- @tparam string mask ---@param mask string
-- @treturn string Masked text ---@return string Masked text
local function mask_text(text, mask) local function mask_text(text, mask)
mask = mask or "*" mask = mask or "*"
local masked_text = "" local masked_text = ""
@@ -132,7 +142,7 @@ end
-- @tfield function on_select (self, button_node) Callback on input field selecting -- @tfield function on_select (self, button_node) Callback on input field selecting
-- @tfield function on_unselect (self, button_node) Callback on input field unselecting -- @tfield function on_unselect (self, button_node) Callback on input field unselecting
-- @tfield function on_input_wrong (self, button_node) Callback on wrong user input -- @tfield function on_input_wrong (self, button_node) Callback on wrong user input
function Input.on_style_change(self, style) function M:on_style_change(style)
self.style = {} self.style = {}
self.style.IS_LONGTAP_ERASE = style.IS_LONGTAP_ERASE or false self.style.IS_LONGTAP_ERASE = style.IS_LONGTAP_ERASE or false
@@ -145,12 +155,11 @@ function Input.on_style_change(self, style)
end end
--- The @{Input} constructor --- The Input constructor
-- @tparam Input self @{Input} ---@param click_node node Node to enabled input component
-- @tparam node click_node Node to enabled input component ---@param text_node node|druid.text Text node what will be changed on user input. You can pass text component instead of text node name Text
-- @tparam node|Text text_node Text node what will be changed on user input. You can pass text component instead of text node name @{Text} ---@param keyboard_type number|nil Gui keyboard type for input field
-- @tparam number|nil keyboard_type Gui keyboard type for input field function M:init(click_node, text_node, keyboard_type)
function Input.init(self, click_node, text_node, keyboard_type)
self.druid = self:get_druid() self.druid = self:get_druid()
if type(text_node) == "table" then if type(text_node) == "table" then
@@ -191,18 +200,18 @@ function Input.init(self, click_node, text_node, keyboard_type)
self.button:set_web_user_interaction(true) self.button:set_web_user_interaction(true)
end end
self.on_input_select = Event() self.on_input_select = event.create()
self.on_input_unselect = Event() self.on_input_unselect = event.create()
self.on_input_text = Event() self.on_input_text = event.create()
self.on_input_empty = Event() self.on_input_empty = event.create()
self.on_input_full = Event() self.on_input_full = event.create()
self.on_input_wrong = Event() self.on_input_wrong = event.create()
self.on_select_cursor_change = Event() self.on_select_cursor_change = event.create()
end end
function Input.on_input(self, action_id, action) function M:on_input(action_id, action)
if not (action_id == nil or Input.ALLOWED_ACTIONS[action_id]) then if not (action_id == nil or M.ALLOWED_ACTIONS[action_id]) then
return false return false
end end
@@ -299,17 +308,17 @@ function Input.on_input(self, action_id, action)
end end
function Input.on_focus_lost(self) function M:on_focus_lost()
self:unselect() self:unselect()
end end
function Input.on_input_interrupt(self) function M:on_input_interrupt()
--self:unselect() --self:unselect()
end end
function Input.get_text_selected(self) function M:get_text_selected()
if self.start_index == self.end_index then if self.start_index == self.end_index then
return self.value return self.value
end end
@@ -318,10 +327,9 @@ function Input.get_text_selected(self)
end end
--- Replace selected text with new text --- Replace selected text with new text
-- @tparam Input self @{Input} ---@param text string The text to replace selected text
-- @tparam string text The text to replace selected text ---@return string New input text
-- @treturn string New input text function M:get_text_selected_replaced(text)
function Input.get_text_selected_replaced(self, text)
local left_part = utf8.sub(self.value, 1, self.start_index) local left_part = utf8.sub(self.value, 1, self.start_index)
local right_part = utf8.sub(self.value, self.end_index + 1, utf8.len(self.value)) local right_part = utf8.sub(self.value, self.end_index + 1, utf8.len(self.value))
local result = left_part .. text .. right_part local result = left_part .. text .. right_part
@@ -336,9 +344,8 @@ end
--- Set text for input field --- Set text for input field
-- @tparam Input self @{Input} ---@param input_text string The string to apply for input field
-- @tparam string input_text The string to apply for input field function M:set_text(input_text)
function Input.set_text(self, input_text)
input_text = tostring(input_text or "") input_text = tostring(input_text or "")
-- Case when update with marked text -- Case when update with marked text
@@ -366,7 +373,7 @@ function Input.set_text(self, input_text)
self.is_empty = #value == 0 and #marked_value == 0 self.is_empty = #value == 0 and #marked_value == 0
local final_text = value .. marked_value local final_text = value .. marked_value
self.text:set_to(final_text) self.text:set_text(final_text)
-- measure it -- measure it
self.text_width = self.text:get_text_size(value) self.text_width = self.text:get_text_size(value)
@@ -385,8 +392,7 @@ end
--- Select input field. It will show the keyboard and trigger on_select events --- Select input field. It will show the keyboard and trigger on_select events
-- @tparam Input self @{Input} function M:select()
function Input.select(self)
gui.reset_keyboard() gui.reset_keyboard()
self.marked_value = "" self.marked_value = ""
if not self.is_selected then if not self.is_selected then
@@ -410,8 +416,7 @@ end
--- Remove selection from input. It will hide the keyboard and trigger on_unselect events --- Remove selection from input. It will hide the keyboard and trigger on_unselect events
-- @tparam Input self @{Input} function M:unselect()
function Input.unselect(self)
gui.reset_keyboard() gui.reset_keyboard()
self.marked_value = "" self.marked_value = ""
self.value = self.current_value self.value = self.current_value
@@ -429,9 +434,8 @@ end
--- Return current input field text --- Return current input field text
-- @tparam Input self @{Input} ---@return string The current input field text
-- @treturn string The current input field text function M:get_text()
function Input.get_text(self)
if self.marked_value ~= "" then if self.marked_value ~= "" then
return self.value .. self.marked_value return self.value .. self.marked_value
end end
@@ -442,10 +446,9 @@ end
--- Set maximum length for input field. --- Set maximum length for input field.
-- Pass nil to make input field unliminted (by default) -- Pass nil to make input field unliminted (by default)
-- @tparam Input self @{Input} ---@param max_length number Maximum length for input text field
-- @tparam number max_length Maximum length for input text field ---@return druid.input Current input instance
-- @treturn druid.input Current input instance function M:set_max_length(max_length)
function Input.set_max_length(self, max_length)
self.max_length = max_length self.max_length = max_length
return self return self
end end
@@ -454,19 +457,17 @@ end
--- Set allowed charaters for input field. --- Set allowed charaters for input field.
-- See: https://defold.com/ref/stable/string/ -- See: https://defold.com/ref/stable/string/
-- ex: [%a%d] for alpha and numeric -- ex: [%a%d] for alpha and numeric
-- @tparam Input self @{Input} ---@param characters string Regulax exp. for validate user input
-- @tparam string characters Regulax exp. for validate user input ---@return druid.input Current input instance
-- @treturn druid.input Current input instance function M:set_allowed_characters(characters)
function Input.set_allowed_characters(self, characters)
self.allowed_characters = characters self.allowed_characters = characters
return self return self
end end
--- Reset current input selection and return previous value --- Reset current input selection and return previous value
-- @tparam Input self @{Input} ---@return druid.input Current input instance
-- @treturn druid.input Current input instance function M:reset_changes()
function Input.reset_changes(self)
self:set_text(self.previous_value) self:set_text(self.previous_value)
self:unselect() self:unselect()
return self return self
@@ -474,12 +475,11 @@ end
--- Set cursor position in input field --- Set cursor position in input field
-- @tparam Input self @{Input} ---@param cursor_index number|nil Cursor index for cursor position, if nil - will be set to the end of the text
-- @tparam number|nil cursor_index Cursor index for cursor position, if nil - will be set to the end of the text ---@param start_index number|nil Start index for cursor position, if nil - will be set to the end of the text
-- @tparam number|nil start_index Start index for cursor position, if nil - will be set to the end of the text ---@param end_index number|nil End index for cursor position, if nil - will be set to the start_index
-- @tparam number|nil end_index End index for cursor position, if nil - will be set to the start_index ---@return druid.input Current input instance
-- @treturn druid.input Current input instance function M:select_cursor(cursor_index, start_index, end_index)
function Input.select_cursor(self, cursor_index, start_index, end_index)
local len = utf8.len(self.value) local len = utf8.len(self.value)
self.cursor_index = cursor_index or len self.cursor_index = cursor_index or len
@@ -497,11 +497,10 @@ end
--- Change cursor position by delta --- Change cursor position by delta
-- @tparam Input self @{Input} ---@param delta number side for cursor position, -1 for left, 1 for right
-- @tparam number delta side for cursor position, -1 for left, 1 for right ---@param is_add_to_selection boolean (Shift key)
-- @tparam boolean is_add_to_selection (Shift key) ---@param is_move_to_end boolean (Ctrl key)
-- @tparam boolean is_move_to_end (Ctrl key) function M:move_selection(delta, is_add_to_selection, is_move_to_end)
function Input.move_selection(self, delta, is_add_to_selection, is_move_to_end)
local len = utf8.len(self.value) local len = utf8.len(self.value)
local cursor_index = self.cursor_index local cursor_index = self.cursor_index
local start_index, end_index -- if nil, the selection will be 0 at cursor position local start_index, end_index -- if nil, the selection will be 0 at cursor position
@@ -559,4 +558,4 @@ function Input.move_selection(self, delta, is_add_to_selection, is_move_to_end)
end end
return Input return M

View File

@@ -18,35 +18,40 @@
-- @alias druid.lang_text -- @alias druid.lang_text
--- On change text callback --- On change text callback
-- @tfield DruidEvent on_change @{DruidEvent} -- @tfield event on_change event
--- The text component --- The text component
-- @tfield Text text @{Text} -- @tfield Text text Text
--- Text node --- Text node
-- @tfield node node -- @tfield node node
--- ---
local Event = require("druid.event") local event = require("event.event")
local settings = require("druid.system.settings") local settings = require("druid.system.settings")
local component = require("druid.component") local component = require("druid.component")
local LangText = component.create("lang_text") ---@class druid.lang_text: druid.base_component
---@field text druid.text
---@field node node
---@field on_change event
---@field private last_locale_args table
---@field private last_locale string
local M = component.create("lang_text")
--- The @{LangText} constructor --- The LangText constructor
-- @tparam LangText self @{LangText} ---@param node string|node The node_id or gui.get_node(node_id)
-- @tparam string|node node The node_id or gui.get_node(node_id) ---@param locale_id string|nil Default locale id or text from node as default
-- @tparam string|nil locale_id Default locale id or text from node as default ---@param adjust_type string|nil Adjust type for text. By default is DOWNSCALE. Look const.TEXT_ADJUST for reference
-- @tparam string|nil adjust_type Adjust type for text. By default is DOWNSCALE. Look const.TEXT_ADJUST for reference function M:init(node, locale_id, adjust_type)
function LangText.init(self, node, locale_id, adjust_type)
self.druid = self:get_druid() self.druid = self:get_druid()
self.text = self.druid:new_text(node, locale_id, adjust_type) self.text = self.druid:new_text(node, locale_id, adjust_type)
self.node = self.text.node self.node = self.text.node
self.last_locale_args = {} self.last_locale_args = {}
self.on_change = Event() self.on_change = event.create()
self:translate(locale_id or gui.get_text(self.node)) self:translate(locale_id or gui.get_text(self.node))
self.text.on_set_text:subscribe(self.on_change.trigger, self.on_change) self.text.on_set_text:subscribe(self.on_change.trigger, self.on_change)
@@ -55,7 +60,7 @@ function LangText.init(self, node, locale_id, adjust_type)
end end
function LangText.on_language_change(self) function M:on_language_change()
if self.last_locale then if self.last_locale then
self:translate(self.last_locale, unpack(self.last_locale_args)) self:translate(self.last_locale, unpack(self.last_locale_args))
end end
@@ -63,53 +68,58 @@ end
--- Setup raw text to lang_text component --- Setup raw text to lang_text component
-- @tparam LangText self @{LangText} ---@param text string Text for text node
-- @tparam string text Text for text node ---@return druid.lang_text Current instance
-- @treturn LangText Current instance function M:set_to(text)
function LangText.set_to(self, text) self.last_locale = nil
self.last_locale = false self.text:set_text(text)
self.text:set_to(text)
self.on_change:trigger() self.on_change:trigger()
return self return self
end end
--- Setup raw text to lang_text component
---@param text string Text for text node
---@return druid.lang_text Current instance
function M:set_text(text)
return self:set_to(text)
end
--- Translate the text by locale_id --- Translate the text by locale_id
-- @tparam LangText self @{LangText} ---@param locale_id string Locale id
-- @tparam string locale_id Locale id ---@param a string|nil Optional param to string.format
-- @tparam string|nil a Optional param to string.format ---@param b string|nil Optional param to string.format
-- @tparam string|nil b Optional param to string.format ---@param c string|nil Optional param to string.format
-- @tparam string|nil c Optional param to string.format ---@param d string|nil Optional param to string.format
-- @tparam string|nil d Optional param to string.format ---@param e string|nil Optional param to string.format
-- @tparam string|nil e Optional param to string.format ---@param f string|nil Optional param to string.format
-- @tparam string|nil f Optional param to string.format ---@param g string|nil Optional param to string.format
-- @tparam string|nil g Optional param to string.format ---@return druid.lang_text Current instance
-- @treturn LangText Current instance function M:translate(locale_id, a, b, c, d, e, f, g)
function LangText.translate(self, locale_id, a, b, c, d, e, f, g)
self.last_locale_args = { a, b, c, d, e, f, g } self.last_locale_args = { a, b, c, d, e, f, g }
self.last_locale = locale_id or self.last_locale self.last_locale = locale_id or self.last_locale
self.text:set_to(settings.get_text(self.last_locale, a, b, c, d, e, f, g) or "") self.text:set_text(settings.get_text(self.last_locale, a, b, c, d, e, f, g) or "")
return self return self
end end
--- Format string with new text params on localized text --- Format string with new text params on localized text
-- @tparam LangText self @{LangText} ---@param a string|nil Optional param to string.format
-- @tparam string|nil a Optional param to string.format ---@param b string|nil Optional param to string.format
-- @tparam string|nil b Optional param to string.format ---@param c string|nil Optional param to string.format
-- @tparam string|nil c Optional param to string.format ---@param d string|nil Optional param to string.format
-- @tparam string|nil d Optional param to string.format ---@param e string|nil Optional param to string.format
-- @tparam string|nil e Optional param to string.format ---@param f string|nil Optional param to string.format
-- @tparam string|nil f Optional param to string.format ---@param g string|nil Optional param to string.format
-- @tparam string|nil g Optional param to string.format ---@return druid.lang_text Current instance
-- @treturn LangText Current instance function M:format(a, b, c, d, e, f, g)
function LangText.format(self, a, b, c, d, e, f, g)
self.last_locale_args = { a, b, c, d, e, f, g } self.last_locale_args = { a, b, c, d, e, f, g }
self.text:set_to(settings.get_text(self.last_locale, a, b, c, d, e, f, g) or "") self.text:set_text(settings.get_text(self.last_locale, a, b, c, d, e, f, g) or "")
return self return self
end end
return LangText return M

View File

@@ -1,56 +1,66 @@
-- Copyright (c) 2024 Maksim Tuprikov <insality@gmail.com>. This code is licensed under MIT license local event = require("event.event")
--- Layout management on node
--
-- <a href="https://insality.github.io/druid/druid/index.html?example=general_layout" target="_blank"><b>Example Link</b></a>
-- @module Layout
-- @within BaseComponent
-- @alias druid.layout
--- Layout node
-- @tfield node node
--- Current layout mode
-- @tfield string mode
---
local helper = require("druid.helper") local helper = require("druid.helper")
local component = require("druid.component") local component = require("druid.component")
-- @class druid.layout.row_data ---@alias druid.layout.mode "horizontal"|"vertical"|"horizontal_wrap"
-- @tfield width number
-- @tfield height number
-- @tfield count number
-- @class druid.layout.rows_data ---@class event.on_size_changed: event
-- @tfield total_width number ---@field subscribe fun(_, callback: fun(new_size: vector3), context: any|nil)
-- @tfield total_height number
-- @tfield nodes_width table<node, number>
-- @tfield nodes_height table<node, number>
-- @tfield rows druid.layout.row_data[]>
-- @class druid.layout: druid.base_component ---@class druid.layout.row_data
---@field width number
---@field height number
---@field count number
---@class druid.layout.rows_data
---@field total_width number
---@field total_height number
---@field nodes_width table<node, number>
---@field nodes_height table<node, number>
---@field rows druid.layout.row_data[]>
---@class druid.layout: druid.base_component
---@field node node
---@field rows_data druid.layout.rows_data Last calculated rows data
---@field is_dirty boolean
---@field entities node[]
---@field margin {x: number, y: number}
---@field padding vector4
---@field type string
---@field is_resize_width boolean
---@field is_resize_height boolean
---@field is_justify boolean
---@field on_size_changed event.on_size_changed
local M = component.create("layout") local M = component.create("layout")
-- The @{Layout} constructor ---Layout component constructor
-- @tparam Layout self @{Layout} ---@local
-- @tparam node node Gui node ---@param node_or_node_id node|string
-- @tparam string layout_type The layout mode (from const.LAYOUT_MODE) ---@param layout_type druid.layout.mode
-- @tparam function|nil on_size_changed_callback The callback on window resize function M:init(node_or_node_id, layout_type)
function M.init(self, node, layout_type) self.node = self:get_node(node_or_node_id)
self.node = self:get_node(node)
self.is_dirty = true self.is_dirty = true
self.entities = {} self.entities = {}
self.margin = { x = 0, y = 0 } self.size = gui.get_size(self.node)
self.padding = gui.get_slice9(self.node) self.padding = gui.get_slice9(self.node)
-- Grab default margins from slice9 z/w values
self.margin = { x = self.padding.z, y = self.padding.w }
-- Use symmetrical padding from x/z
self.padding.z = self.padding.x
self.padding.w = self.padding.y
self.type = layout_type or "horizontal" self.type = layout_type or "horizontal"
self.is_resize_width = false self.is_resize_width = false
self.is_resize_height = false self.is_resize_height = false
self.is_justify = false self.is_justify = false
self.on_size_changed = event.create() --[[@as event.on_size_changed]]
end end
---@local
function M:update() function M:update()
if not self.is_dirty then if not self.is_dirty then
return return
@@ -60,11 +70,26 @@ function M:update()
end end
-- @tparam Layout self @{Layout} function M:get_entities()
-- @tparam number|nil margin_x return self.entities
-- @tparam number|nil margin_y end
-- @treturn druid.layout @{Layout}
function M.set_margin(self, margin_x, margin_y)
function M:set_node_index(node, index)
for i = 1, #self.entities do
if self.entities[i] == node then
table.remove(self.entities, i)
table.insert(self.entities, index, node)
break
end
end
end
---@param margin_x number|nil
---@param margin_y number|nil
---@return druid.layout
function M:set_margin(margin_x, margin_y)
self.margin.x = margin_x or self.margin.x self.margin.x = margin_x or self.margin.x
self.margin.y = margin_y or self.margin.y self.margin.y = margin_y or self.margin.y
self.is_dirty = true self.is_dirty = true
@@ -73,30 +98,33 @@ function M.set_margin(self, margin_x, margin_y)
end end
-- @tparam Layout self @{Layout} ---@param padding_x number|nil
-- @tparam vector4 padding The vector4 with padding values, where x - left, y - top, z - right, w - bottom ---@param padding_y number|nil
-- @treturn druid.layout @{Layout} ---@param padding_z number|nil
function M.set_padding(self, padding) ---@param padding_w number|nil
self.padding = padding ---@return druid.layout
function M:set_padding(padding_x, padding_y, padding_z, padding_w)
self.padding.x = padding_x or self.padding.x
self.padding.y = padding_y or self.padding.y
self.padding.z = padding_z or self.padding.z
self.padding.w = padding_w or self.padding.w
self.is_dirty = true self.is_dirty = true
return self return self
end end
-- @tparam Layout self @{Layout} ---@return druid.layout
-- @treturn druid.layout @{Layout} function M:set_dirty()
function M.set_dirty(self)
self.is_dirty = true self.is_dirty = true
return self return self
end end
-- @tparam Layout self @{Layout} ---@param is_justify boolean
-- @tparam boolean is_justify ---@return druid.layout
-- @treturn druid.layout @{Layout} function M:set_justify(is_justify)
function M.set_justify(self, is_justify)
self.is_justify = is_justify self.is_justify = is_justify
self.is_dirty = true self.is_dirty = true
@@ -104,10 +132,9 @@ function M.set_justify(self, is_justify)
end end
-- @tparam Layout self @{Layout} ---@param type string The layout type: "horizontal", "vertical", "horizontal_wrap"
-- @tparam string type The layout type: "horizontal", "vertical", "horizontal_wrap" ---@return druid.layout
-- @treturn druid.layout @{Layout} function M:set_type(type)
function M.set_type(self, type)
self.type = type self.type = type
self.is_dirty = true self.is_dirty = true
@@ -115,11 +142,10 @@ function M.set_type(self, type)
end end
-- @tparam Layout self @{Layout} ---@param is_hug_width boolean
-- @tparam boolean is_hug_width ---@param is_hug_height boolean
-- @tparam boolean is_hug_height ---@return druid.layout
-- @treturn druid.layout @{Layout} function M:set_hug_content(is_hug_width, is_hug_height)
function M.set_hug_content(self, is_hug_width, is_hug_height)
self.is_resize_width = is_hug_width or false self.is_resize_width = is_hug_width or false
self.is_resize_height = is_hug_height or false self.is_resize_height = is_hug_height or false
self.is_dirty = true self.is_dirty = true
@@ -128,33 +154,63 @@ function M.set_hug_content(self, is_hug_width, is_hug_height)
end end
-- @tparam Layout self @{Layout} ---Add node to layout
-- @tparam string|node node_or_node_id ---@param node_or_node_id node|string node_or_node_id
-- @treturn druid.layout @{Layout} ---@return druid.layout
function M.add(self, node_or_node_id) function M:add(node_or_node_id)
-- Acquire node from entity or by id -- Acquire node from entity or by id
local node = node_or_node_id local node = node_or_node_id
if type(node_or_node_id) == "table" then if type(node_or_node_id) == "table" then
assert(node_or_node_id.node, "The entity should have a node") assert(node_or_node_id.node, "The entity should have a node")
node = node_or_node_id.node node = node_or_node_id.node
else else
-- @cast node_or_node_id string|node ---@cast node_or_node_id string|node
node = self:get_node(node_or_node_id) node = self:get_node(node_or_node_id)
end end
-- @cast node node ---@cast node node
table.insert(self.entities, node) table.insert(self.entities, node)
gui.set_parent(node, self.node) gui.set_parent(node, self.node)
self.is_dirty = true self.is_dirty = true
return self return self
end end
-- @tparam Layout self @{Layout} ---Remove node from layout
-- @treturn druid.layout @{Layout} ---@param node_or_node_id node|string node_or_node_id
function M.refresh_layout(self) ---@return druid.layout self for chaining
function M:remove(node_or_node_id)
local node = type(node_or_node_id) == "table" and node_or_node_id.node or self:get_node(node_or_node_id)
for index = #self.entities, 1, -1 do
if self.entities[index] == node then
table.remove(self.entities, index)
self.is_dirty = true
break
end
end
return self
end
---@return vector3
function M:get_size()
return self.size
end
---@return number, number
function M:get_content_size()
local width = self.size.x - self.padding.x - self.padding.z
local height = self.size.y - self.padding.y - self.padding.w
return width, height
end
---@return druid.layout
function M:refresh_layout()
local layout_node = self.node local layout_node = self.node
local entities = self.entities local entities = self.entities
@@ -168,6 +224,7 @@ function M.refresh_layout(self)
local layout_pivot_offset = helper.get_pivot_offset(gui.get_pivot(layout_node)) -- {x: -0.5, y: -0.5} - is left bot, {x: 0.5, y: 0.5} - is right top local layout_pivot_offset = helper.get_pivot_offset(gui.get_pivot(layout_node)) -- {x: -0.5, y: -0.5} - is left bot, {x: 0.5, y: 0.5} - is right top
local rows_data = self:calculate_rows_data() local rows_data = self:calculate_rows_data()
self.rows_data = rows_data
local rows = rows_data.rows local rows = rows_data.rows
local row_index = 1 local row_index = 1
local row = rows[row_index] local row = rows[row_index]
@@ -191,7 +248,7 @@ function M.refresh_layout(self)
local node_height = rows_data.nodes_height[node] local node_height = rows_data.nodes_height[node]
local pivot_offset = helper.get_pivot_offset(gui.get_pivot(node)) local pivot_offset = helper.get_pivot_offset(gui.get_pivot(node))
if node_width > 0 and node_height > 0 then if node_width > 0 or node_height > 0 then
-- Calculate position for current node -- Calculate position for current node
local position_x, position_y local position_x, position_y
@@ -281,6 +338,9 @@ function M.refresh_layout(self)
size.y = rows_data.total_height + padding.y + padding.w size.y = rows_data.total_height + padding.y + padding.w
end end
gui.set_size(layout_node, size) gui.set_size(layout_node, size)
self.size = size
self.on_size_changed(size)
end end
self.is_dirty = false self.is_dirty = false
@@ -289,9 +349,8 @@ function M.refresh_layout(self)
end end
-- @tparam Layout self @{Layout} ---@return druid.layout
-- @treturn druid.layout @{Layout} function M:clear_layout()
function M.clear_layout(self)
for index = #self.entities, 1, -1 do for index = #self.entities, 1, -1 do
self.entities[index] = nil self.entities[index] = nil
end end
@@ -302,10 +361,9 @@ function M.clear_layout(self)
end end
-- @tparam node node ---@param node node
-- @treturn number, number ---@return number, number
-- @local function M:get_node_size(node)
function M.get_node_size(node)
if not gui.is_enabled(node, false) then if not gui.is_enabled(node, false) then
return 0, 0 return 0, 0
end end
@@ -323,11 +381,10 @@ function M.get_node_size(node)
end end
-- @tparam Layout self @{Layout} ---Calculate rows data for layout. Contains total width, height and rows info (width, height, count of elements in row)
-- Calculate rows data for layout. Contains total width, height and rows info (width, height, count of elements in row) ---@local
-- @treturn druid.layout.rows_data ---@return druid.layout.rows_data
-- @local function M:calculate_rows_data()
function M.calculate_rows_data(self)
local entities = self.entities local entities = self.entities
local margin = self.margin local margin = self.margin
local type = self.type local type = self.type
@@ -353,12 +410,12 @@ function M.calculate_rows_data(self)
-- Get node size if it's not calculated yet -- Get node size if it's not calculated yet
if not node_width or not node_height then if not node_width or not node_height then
node_width, node_height = M.get_node_size(node) node_width, node_height = self:get_node_size(node)
rows_data.nodes_width[node] = node_width rows_data.nodes_width[node] = node_width
rows_data.nodes_height[node] = node_height rows_data.nodes_height[node] = node_height
end end
if node_width > 0 and node_height > 0 then if node_width > 0 or node_height > 0 then
if type == "horizontal" then if type == "horizontal" then
current_row.width = current_row.width + node_width + margin.x current_row.width = current_row.width + node_width + margin.x
current_row.height = math.max(current_row.height, node_height) current_row.height = math.max(current_row.height, node_height)
@@ -407,16 +464,16 @@ function M.calculate_rows_data(self)
end end
-- @tparam node node ---Will reset z value to 0!
-- @tparam number x local TEMP_VECTOR = vmath.vector3(0, 0, 0)
-- @tparam number y ---@param node node
-- @treturn node ---@param x number
-- @local ---@param y number
---@return node
function M:set_node_position(node, x, y) function M:set_node_position(node, x, y)
local position = gui.get_position(node) TEMP_VECTOR.x = x
position.x = x TEMP_VECTOR.y = y
position.y = y gui.set_position(node, TEMP_VECTOR)
gui.set_position(node, position)
return node return node
end end

View File

@@ -20,7 +20,7 @@
-- @alias druid.progress -- @alias druid.progress
--- On progress bar change callback(self, new_value) --- On progress bar change callback(self, new_value)
-- @tfield DruidEvent on_change @{DruidEvent} -- @tfield event on_change event
--- Progress bar fill node --- Progress bar fill node
-- @tfield node node -- @tfield node node
@@ -44,12 +44,18 @@
--- ---
local Event = require("druid.event") local event = require("event.event")
local const = require("druid.const") local const = require("druid.const")
local helper = require("druid.helper") local helper = require("druid.helper")
local component = require("druid.component") local component = require("druid.component")
local Progress = component.create("progress") ---@class druid.progress: druid.base_component
---@field node node
---@field on_change event
---@field style table
---@field key string
---@field prop hash
local M = component.create("progress")
local function check_steps(self, from, to, exactly) local function check_steps(self, from, to, exactly)
@@ -117,19 +123,18 @@ end
-- @table style -- @table style
-- @tfield number|nil SPEED Progress bas fill rate. More -> faster. Default: 5 -- @tfield number|nil SPEED Progress bas fill rate. More -> faster. Default: 5
-- @tfield number|nil MIN_DELTA Minimum step to fill progress bar. Default: 0.005 -- @tfield number|nil MIN_DELTA Minimum step to fill progress bar. Default: 0.005
function Progress.on_style_change(self, style) function M:on_style_change(style)
self.style = {} self.style = {}
self.style.SPEED = style.SPEED or 5 self.style.SPEED = style.SPEED or 5
self.style.MIN_DELTA = style.MIN_DELTA or 0.005 self.style.MIN_DELTA = style.MIN_DELTA or 0.005
end end
--- The @{Progress} constructor --- The Progress constructor
-- @tparam Progress self @{Progress} ---@param node string|node Node name or GUI Node itself.
-- @tparam string|node node Node name or GUI Node itself. ---@param key string Progress bar direction: const.SIDE.X or const.SIDE.Y
-- @tparam string key Progress bar direction: const.SIDE.X or const.SIDE.Y ---@param init_value number|nil Initial value of progress bar. Default: 1
-- @tparam number|nil init_value Initial value of progress bar. Default: 1 function M:init(node, key, init_value)
function Progress.init(self, node, key, init_value)
assert(key == const.SIDE.X or const.SIDE.Y, "Progress bar key should be 'x' or 'y'") assert(key == const.SIDE.X or const.SIDE.Y, "Progress bar key should be 'x' or 'y'")
self.key = key self.key = key
@@ -149,24 +154,24 @@ function Progress.init(self, node, key, init_value)
0 0
) )
self.on_change = Event() self.on_change = event.create()
self:set_to(self.last_value) self:set_to(self.last_value)
end end
function Progress.on_layout_change(self) function M:on_layout_change()
self:set_to(self.last_value) self:set_to(self.last_value)
end end
function Progress.on_remove(self) function M:on_remove()
-- Return default size -- Return default size
gui.set_size(self.node, self.max_size) gui.set_size(self.node, self.max_size)
end end
function Progress.update(self, dt) function M:update(dt)
if self.target then if self.target then
local prev_value = self.last_value local prev_value = self.last_value
local step = math.abs(self.last_value - self.target) * (self.style.SPEED*dt) local step = math.abs(self.last_value - self.target) * (self.style.SPEED*dt)
@@ -187,51 +192,45 @@ end
--- Fill a progress bar and stop progress animation --- Fill a progress bar and stop progress animation
-- @tparam Progress self @{Progress} function M:fill()
function Progress.fill(self)
set_bar_to(self, 1, true) set_bar_to(self, 1, true)
end end
--- Empty a progress bar --- Empty a progress bar
-- @tparam Progress self @{Progress} function M:empty()
function Progress.empty(self)
set_bar_to(self, 0, true) set_bar_to(self, 0, true)
end end
--- Instant fill progress bar to value --- Instant fill progress bar to value
-- @tparam Progress self @{Progress} ---@param to number Progress bar value, from 0 to 1
-- @tparam number to Progress bar value, from 0 to 1 function M:set_to(to)
function Progress.set_to(self, to)
to = helper.clamp(to, 0, 1) to = helper.clamp(to, 0, 1)
set_bar_to(self, to) set_bar_to(self, to)
end end
--- Return current progress bar value --- Return current progress bar value
-- @tparam Progress self @{Progress} function M:get()
function Progress.get(self)
return self.last_value return self.last_value
end end
--- Set points on progress bar to fire the callback --- Set points on progress bar to fire the callback
-- @tparam Progress self @{Progress} ---@param steps number[] Array of progress bar values
-- @tparam number[] steps Array of progress bar values ---@param callback function Callback on intersect step value
-- @tparam function callback Callback on intersect step value
-- @usage progress:set_steps({0, 0.3, 0.6, 1}, function(self, step) end) -- @usage progress:set_steps({0, 0.3, 0.6, 1}, function(self, step) end)
function Progress.set_steps(self, steps, callback) function M:set_steps(steps, callback)
self.steps = steps self.steps = steps
self.step_callback = callback self.step_callback = callback
end end
--- Start animation of a progress bar --- Start animation of a progress bar
-- @tparam Progress self @{Progress} ---@param to number value between 0..1
-- @tparam number to value between 0..1 ---@param callback function|nil Callback on animation ends
-- @tparam function|nil callback Callback on animation ends function M:to(to, callback)
function Progress.to(self, to, callback)
to = helper.clamp(to, 0, 1) to = helper.clamp(to, 0, 1)
-- cause of float error -- cause of float error
local value = helper.round(to, 5) local value = helper.round(to, 5)
@@ -247,14 +246,13 @@ end
--- Set progress bar max node size --- Set progress bar max node size
-- @tparam Progress self @{Progress} ---@param max_size vector3 The new node maximum (full) size
-- @tparam vector3 max_size The new node maximum (full) size ---@return druid.progress Progress
-- @treturn Progress @{Progress} function M:set_max_size(max_size)
function Progress.set_max_size(self, max_size)
self.max_size[self.key] = max_size[self.key] self.max_size[self.key] = max_size[self.key]
self:set_to(self.last_value) self:set_to(self.last_value)
return self return self
end end
return Progress return M

View File

@@ -8,7 +8,7 @@
-- @alias druid.slider -- @alias druid.slider
--- On change value callback(self, value) --- On change value callback(self, value)
-- @tfield DruidEvent on_change_value @{DruidEvent} -- @tfield event on_change_value event
--- Slider pin node --- Slider pin node
-- @tfield node node -- @tfield node node
@@ -37,12 +37,24 @@
--- ---
local Event = require("druid.event") local event = require("event.event")
local helper = require("druid.helper") local helper = require("druid.helper")
local const = require("druid.const") local const = require("druid.const")
local component = require("druid.component") local component = require("druid.component")
local Slider = component.create("slider", const.PRIORITY_INPUT_HIGH) ---@class druid.slider: druid.base_component
---@field node node
---@field on_change_value event
---@field style table
---@field private start_pos vector3
---@field private pos vector3
---@field private target_pos vector3
---@field private end_pos vector3
---@field private dist vector3
---@field private is_drag boolean
---@field private value number
---@field private steps number[]
local M = component.create("slider", const.PRIORITY_INPUT_HIGH)
local function on_change_value(self) local function on_change_value(self)
@@ -56,12 +68,11 @@ local function set_position(self, value)
end end
--- The @{Slider} constructor --- The Slider constructor
-- @tparam Slider self @{Slider} ---@param node node Gui pin node
-- @tparam node node Gui pin node ---@param end_pos vector3 The end position of slider
-- @tparam vector3 end_pos The end position of slider ---@param callback function|nil On slider change callback
-- @tparam function|nil callback On slider change callback function M:init(node, end_pos, callback)
function Slider.init(self, node, end_pos, callback)
self.node = self:get_node(node) self.node = self:get_node(node)
self.start_pos = gui.get_position(self.node) self.start_pos = gui.get_position(self.node)
@@ -74,25 +85,25 @@ function Slider.init(self, node, end_pos, callback)
self.is_drag = false self.is_drag = false
self.value = 0 self.value = 0
self.on_change_value = Event(callback) self.on_change_value = event.create(callback)
self:on_window_resized() self:on_window_resized()
assert(self.dist.x == 0 or self.dist.y == 0, "Slider for now can be only vertical or horizontal") assert(self.dist.x == 0 or self.dist.y == 0, "Slider for now can be only vertical or horizontal")
end end
function Slider.on_layout_change(self) function M:on_layout_change()
self:set(self.value) self:set(self.value)
end end
function Slider.on_remove(self) function M:on_remove()
-- Return pin to start position -- Return pin to start position
gui.set_position(self.node, self.start_pos) gui.set_position(self.node, self.start_pos)
end end
function Slider.on_window_resized(self) function M:on_window_resized()
local x_koef, y_koef = helper.get_screen_aspect_koef() local x_koef, y_koef = helper.get_screen_aspect_koef()
self._x_koef = x_koef self._x_koef = x_koef
self._y_koef = y_koef self._y_koef = y_koef
@@ -100,7 +111,7 @@ function Slider.on_window_resized(self)
end end
function Slider.on_input(self, action_id, action) function M:on_input(action_id, action)
if action_id ~= const.ACTION_TOUCH then if action_id ~= const.ACTION_TOUCH then
return false return false
end end
@@ -185,10 +196,9 @@ end
--- Set value for slider --- Set value for slider
-- @tparam Slider self @{Slider} ---@param value number Value from 0 to 1
-- @tparam number value Value from 0 to 1 ---@param is_silent boolean|nil Don't trigger event if true
-- @tparam boolean|nil is_silent Don't trigger event if true function M:set(value, is_silent)
function Slider.set(self, value, is_silent)
value = helper.clamp(value, 0, 1) value = helper.clamp(value, 0, 1)
set_position(self, value) set_position(self, value)
self.value = value self.value = value
@@ -200,11 +210,10 @@ end
--- Set slider steps. Pin node will --- Set slider steps. Pin node will
-- apply closest step position -- apply closest step position
-- @tparam Slider self @{Slider} ---@param steps number[] Array of steps
-- @tparam number[] steps Array of steps
-- @usage slider:set_steps({0, 0.2, 0.6, 1}) -- @usage slider:set_steps({0, 0.2, 0.6, 1})
-- @treturn Slider @{Slider} ---@return druid.slider Slider
function Slider.set_steps(self, steps) function M:set_steps(steps)
self.steps = steps self.steps = steps
return self return self
end end
@@ -214,29 +223,31 @@ end
-- User can touch any place of node, pin instantly will -- User can touch any place of node, pin instantly will
-- move at this position and node drag will start. -- move at this position and node drag will start.
-- This function require the Defold version 1.3.0+ -- This function require the Defold version 1.3.0+
-- @tparam Slider self @{Slider} ---@param input_node node|string|nil
-- @tparam node|string|nil input_node ---@return druid.slider Slider
-- @treturn Slider @{Slider} function M:set_input_node(input_node)
function Slider.set_input_node(self, input_node) if not input_node then
self._input_node = nil
return self
end
self._input_node = self:get_node(input_node) self._input_node = self:get_node(input_node)
return self return self
end end
--- Set Slider input enabled or disabled --- Set Slider input enabled or disabled
-- @tparam Slider self @{Slider} ---@param is_enabled boolean
-- @tparam boolean is_enabled function M:set_enabled(is_enabled)
function Slider.set_enabled(self, is_enabled)
self._is_enabled = is_enabled self._is_enabled = is_enabled
end end
--- Check if Slider component is enabled --- Check if Slider component is enabled
-- @tparam Slider self @{Slider} ---@return boolean
-- @treturn boolean function M:is_enabled()
function Slider.is_enabled(self)
return self._is_enabled return self._is_enabled
end end
return Slider return M

View File

@@ -10,22 +10,32 @@
-- @alias druid.swipe -- @alias druid.swipe
--- Swipe node --- Swipe node
-- @tparam node node --@param node node
--- Restriction zone --- Restriction zone
-- @tparam node|nil click_zone --@param click_zone node|nil
--- Trigger on swipe event(self, swipe_side, dist, delta_time) --- Trigger on swipe event(self, swipe_side, dist, delta_time)
-- @tfield DruidEvent on_swipe) @{DruidEvent} --@param event event on_swipe
--- ---
local Event = require("druid.event") local event = require("event.event")
local const = require("druid.const") local const = require("druid.const")
local helper = require("druid.helper") local helper = require("druid.helper")
local component = require("druid.component") local component = require("druid.component")
local Swipe = component.create("swipe") ---@class druid.swipe: druid.base_component
---@field node node
---@field on_swipe event function(side, dist, dt), side - "left", "right", "up", "down"
---@field style table
---@field click_zone node
---@field private _trigger_on_move boolean
---@field private _swipe_start_time number
---@field private _start_pos vector3
---@field private _is_enabled boolean
---@field private _is_mobile boolean
local M = component.create("swipe")
local function start_swipe(self, action) local function start_swipe(self, action)
@@ -36,7 +46,7 @@ end
local function reset_swipe(self, action) local function reset_swipe(self, action)
self._swipe_start_time = false self._swipe_start_time = 0
end end
@@ -49,19 +59,19 @@ local function check_swipe(self, action)
if is_swipe then if is_swipe then
local is_x_swipe = math.abs(dx) >= math.abs(dy) local is_x_swipe = math.abs(dx) >= math.abs(dy)
local swipe_side = false local swipe_side = "undefined"
if is_x_swipe and dx > 0 then if is_x_swipe and dx > 0 then
swipe_side = const.SWIPE.RIGHT swipe_side = "right"
end end
if is_x_swipe and dx < 0 then if is_x_swipe and dx < 0 then
swipe_side = const.SWIPE.LEFT swipe_side = "left"
end end
if not is_x_swipe and dy > 0 then if not is_x_swipe and dy > 0 then
swipe_side = const.SWIPE.UP swipe_side = "up"
end end
if not is_x_swipe and dy < 0 then if not is_x_swipe and dy < 0 then
swipe_side = const.SWIPE.DOWN swipe_side = "down"
end end
self.on_swipe:trigger(self:get_context(), swipe_side, dist, delta_time) self.on_swipe:trigger(self:get_context(), swipe_side, dist, delta_time)
@@ -73,11 +83,13 @@ end
--- Component style params. --- Component style params.
-- You can override this component styles params in druid styles table -- You can override this component styles params in druid styles table
-- or create your own style -- or create your own style
-- @table style ---@class druid.swipe.style
-- @tfield number|nil SWIPE_TIME Maximum time for swipe trigger. Default: 0.4 ---@field SWIPE_TIME number|nil Maximum time for swipe trigger. Default: 0.4
-- @tfield number|nil SWIPE_THRESHOLD Minimum distance for swipe trigger. Default: 50 ---@field SWIPE_THRESHOLD number|nil Minimum distance for swipe trigger. Default: 50
-- @tfield boolean|nil SWIPE_TRIGGER_ON_MOVE If true, trigger on swipe moving, not only release action. Default: false ---@field SWIPE_TRIGGER_ON_MOVE boolean|nil If true, trigger on swipe moving, not only release action. Default: false
function Swipe.on_style_change(self, style)
---@param style druid.swipe.style
function M:on_style_change(style)
self.style = {} self.style = {}
self.style.SWIPE_TIME = style.SWIPE_TIME or 0.4 self.style.SWIPE_TIME = style.SWIPE_TIME or 0.4
self.style.SWIPE_THRESHOLD = style.SWIPE_THRESHOLD or 50 self.style.SWIPE_THRESHOLD = style.SWIPE_THRESHOLD or 50
@@ -85,24 +97,23 @@ function Swipe.on_style_change(self, style)
end end
--- The @{Swipe} constructor ---Swipe constructor
-- @tparam Swipe self @{Swipe} ---@param node_or_node_id node|string
-- @tparam node node Gui node ---@param on_swipe_callback function
-- @tparam function on_swipe_callback Swipe callback for on_swipe_end event function M:init(node_or_node_id, on_swipe_callback)
function Swipe.init(self, node, on_swipe_callback)
self._trigger_on_move = self.style.SWIPE_TRIGGER_ON_MOVE self._trigger_on_move = self.style.SWIPE_TRIGGER_ON_MOVE
self.node = self:get_node(node) self.node = self:get_node(node_or_node_id)
self._swipe_start_time = false self._swipe_start_time = 0
self._start_pos = vmath.vector3(0) self._start_pos = vmath.vector3(0)
self.click_zone = nil self.click_zone = nil
self.on_swipe = Event(on_swipe_callback) self.on_swipe = event.create(on_swipe_callback)
end end
function Swipe.on_late_init(self) function M:on_late_init()
if not self.click_zone and const.IS_STENCIL_CHECK then if not self.click_zone then
local stencil_node = helper.get_closest_stencil_node(self.node) local stencil_node = helper.get_closest_stencil_node(self.node)
if stencil_node then if stencil_node then
self:set_click_zone(stencil_node) self:set_click_zone(stencil_node)
@@ -111,7 +122,9 @@ function Swipe.on_late_init(self)
end end
function Swipe.on_input(self, action_id, action) ---@param action_id hash
---@param action action
function M:on_input(action_id, action)
if action_id ~= const.ACTION_TOUCH then if action_id ~= const.ACTION_TOUCH then
return false return false
end end
@@ -126,7 +139,7 @@ function Swipe.on_input(self, action_id, action)
return false return false
end end
if self._swipe_start_time and (self._trigger_on_move or action.released) then if self._swipe_start_time ~= 0 and (self._trigger_on_move or action.released) then
check_swipe(self, action) check_swipe(self, action)
end end
@@ -142,18 +155,22 @@ function Swipe.on_input(self, action_id, action)
end end
function Swipe.on_input_interrupt(self) function M:on_input_interrupt()
reset_swipe(self) reset_swipe(self)
end end
--- Strict swipe click area. Useful for --- Strict swipe click area. Useful for
-- restrict events outside stencil node -- restrict events outside stencil node
-- @tparam Swipe self @{Swipe} ---@param zone node|string|nil Gui node
-- @tparam node|string|nil zone Gui node function M:set_click_zone(zone)
function Swipe.set_click_zone(self, zone) if not zone then
self.click_zone = nil
return
end
self.click_zone = self:get_node(zone) self.click_zone = self:get_node(zone)
end end
return Swipe return M

View File

@@ -1,40 +1,18 @@
-- Copyright (c) 2021 Maksim Tuprikov <insality@gmail.com>. This code is licensed under MIT license local event = require("event.event")
--- Component to handle GUI timers.
-- Timer updating by game delta time. If game is not focused -
-- timer will be not updated.
-- @module Timer
-- @within BaseComponent
-- @alias druid.timer
--- On timer tick. Fire every second callback(self, value)
-- @tfield DruidEvent on_tick @{DruidEvent}
--- On timer change enabled state callback(self, is_enabled)
-- @tfield DruidEvent on_set_enabled @{DruidEvent}
--- On timer end callback
-- @tfield DruidEvent on_timer_end(self, Timer) @{DruidEvent}
--- Trigger node
-- @tfield node node
--- Initial timer value
-- @tfield number from
--- Target timer value
-- @tfield number target
--- Current timer value
-- @tfield number value
---
local Event = require("druid.event")
local helper = require("druid.helper") local helper = require("druid.helper")
local component = require("druid.component") local component = require("druid.component")
local Timer = component.create("timer") ---@class druid.timer: druid.base_component
---@field on_tick event
---@field on_set_enabled event
---@field on_timer_end event
---@field style table
---@field node node
---@field from number
---@field target number
---@field value number
---@field is_on boolean|nil
local M = component.create("timer")
local function second_string_min(sec) local function second_string_min(sec)
@@ -44,19 +22,18 @@ local function second_string_min(sec)
end end
--- The @{Timer} constructor ---The Timer constructor
-- @tparam Timer self @{Timer} ---@param node node Gui text node
-- @tparam node node Gui text node ---@param seconds_from number|nil Start timer value in seconds
-- @tparam number|nil seconds_from Start timer value in seconds ---@param seconds_to number|nil End timer value in seconds
-- @tparam number|nil seconds_to End timer value in seconds ---@param callback function|nil Function on timer end
-- @tparam function|nil callback Function on timer end function M:init(node, seconds_from, seconds_to, callback)
function Timer.init(self, node, seconds_from, seconds_to, callback)
self.node = self:get_node(node) self.node = self:get_node(node)
seconds_to = math.max(seconds_to or 0, 0) seconds_to = math.max(seconds_to or 0, 0)
self.on_tick = Event() self.on_tick = event.create()
self.on_set_enabled = Event() self.on_set_enabled = event.create()
self.on_timer_end = Event(callback) self.on_timer_end = event.create(callback)
if seconds_from then if seconds_from then
seconds_from = math.max(seconds_from, 0) seconds_from = math.max(seconds_from, 0)
@@ -73,7 +50,7 @@ function Timer.init(self, node, seconds_from, seconds_to, callback)
end end
function Timer.update(self, dt) function M:update(dt)
if not self.is_on then if not self.is_on then
return return
end end
@@ -96,42 +73,44 @@ function Timer.update(self, dt)
end end
function Timer.on_layout_change(self) function M:on_layout_change()
self:set_to(self.last_value) self:set_to(self.last_value)
end end
--- Set text to text field ---@param set_to number Value in seconds
-- @tparam Timer self @{Timer} ---@return druid.timer self
-- @tparam number set_to Value in seconds function M:set_to(set_to)
function Timer.set_to(self, set_to)
self.last_value = set_to self.last_value = set_to
gui.set_text(self.node, second_string_min(set_to)) gui.set_text(self.node, second_string_min(set_to))
return self
end end
--- Called when update ---@param is_on boolean|nil Timer enable state
-- @tparam Timer self @{Timer} ---@return druid.timer self
-- @tparam boolean|nil is_on Timer enable state function M:set_state(is_on)
function Timer.set_state(self, is_on)
self.is_on = is_on self.is_on = is_on
self.on_set_enabled:trigger(self:get_context(), is_on) self.on_set_enabled:trigger(self:get_context(), is_on)
return self
end end
--- Set time interval ---@param from number Start time in seconds
-- @tparam Timer self @{Timer} ---@param to number Target time in seconds
-- @tparam number from Start time in seconds ---@return druid.timer self
-- @tparam number to Target time in seconds function M:set_interval(from, to)
function Timer.set_interval(self, from, to)
self.from = from self.from = from
self.value = from self.value = from
self.temp = 0 self.temp = 0
self.target = to self.target = to
self:set_state(true) self:set_state(true)
self:set_to(from) self:set_to(from)
return self
end end
return Timer return M

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,10 +0,0 @@
font: "/druid/fonts/Roboto-Bold.ttf"
material: "/builtins/fonts/font-df.material"
size: 40
outline_alpha: 1.0
outline_width: 2.0
shadow_alpha: 1.0
shadow_blur: 2
output_format: TYPE_DISTANCE_FIELD
render_mode: MODE_MULTI_LAYER
characters: " !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\302\241\302\277\303\200\303\202\303\206\303\207\303\210\303\211\303\212\303\213\303\216\303\217\303\224\303\231\303\233\303\234\303\240\303\241\303\242\303\243\303\244\303\246\303\247\303\250\303\251\303\252\303\253\303\255\303\256\303\257\303\261\303\263\303\264\303\271\303\273\303\274\303\277\305\222\305\223\305\270\320\201\320\220\320\221\320\222\320\223\320\224\320\225\320\226\320\227\320\230\320\231\320\232\320\233\320\234\320\235\320\236\320\237\320\240\320\241\320\242\320\243\320\244\320\245\320\246\320\247\320\250\320\251\320\252\320\253\320\254\320\255\320\256\320\257\320\260\320\261\320\262\320\263\320\264\320\265\320\266\320\267\320\270\320\271\320\272\320\273\320\274\320\275\320\276\320\277\321\200\321\201\321\202\321\203\321\204\321\205\321\206\321\207\321\210\321\211\321\212\321\213\321\214\321\215\321\216\321\217\321\221\343\200\202\357\274\201\357\274\237"

View File

@@ -1,8 +0,0 @@
font: "/druid/fonts/Roboto-Regular.ttf"
material: "/builtins/fonts/font-df.material"
size: 40
outline_alpha: 1.0
outline_width: 2.0
output_format: TYPE_DISTANCE_FIELD
render_mode: MODE_MULTI_LAYER
characters: " !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\302\241\302\277\303\200\303\202\303\206\303\207\303\210\303\211\303\212\303\213\303\216\303\217\303\224\303\231\303\233\303\234\303\240\303\241\303\242\303\243\303\244\303\246\303\247\303\250\303\251\303\252\303\253\303\255\303\256\303\257\303\261\303\263\303\264\303\271\303\273\303\274\303\277\305\222\305\223\305\270\320\201\320\220\320\221\320\222\320\223\320\224\320\225\320\226\320\227\320\230\320\231\320\232\320\233\320\234\320\235\320\236\320\237\320\240\320\241\320\242\320\243\320\244\320\245\320\246\320\247\320\250\320\251\320\252\320\253\320\254\320\255\320\256\320\257\320\260\320\261\320\262\320\263\320\264\320\265\320\266\320\267\320\270\320\271\320\272\320\273\320\274\320\275\320\276\320\277\321\200\321\201\321\202\321\203\321\204\321\205\321\206\321\207\321\210\321\211\321\212\321\213\321\214\321\215\321\216\321\217\321\221\343\200\202\357\274\201\357\274\237"

View File

@@ -1,24 +1,26 @@
-- Copyright (c) 2021 Maksim Tuprikov <insality@gmail.com>. This code is licensed under MIT license
--- Helper module with various usefull GUI functions.
-- @usage
-- local helper = require("druid.helper")
-- helper.centrate_nodes(0, node_1, node_2)
-- @module Helper
-- @alias druid.helper
local const = require("druid.const") local const = require("druid.const")
-- Localize functions for better performance
local gui_get_node = gui.get_node
local gui_get = gui.get
local gui_pick_node = gui.pick_node
---@class druid.system.helper
local M = {} local M = {}
local POSITION_X = hash("position.x") local POSITION_X = hash("position.x")
local SCALE_X = hash("scale.x") local SCALE_X = hash("scale.x")
local SIZE_X = hash("size.x") local SIZE_X = hash("size.x")
M.PROP_SIZE_X = hash("size.x")
M.PROP_SIZE_Y = hash("size.y")
M.PROP_SCALE_X = hash("scale.x")
M.PROP_SCALE_Y = hash("scale.y")
local function get_text_width(text_node) local function get_text_width(text_node)
if text_node then if text_node then
local text_metrics = M.get_text_metrics_from_node(text_node) local text_metrics = M.get_text_metrics_from_node(text_node)
local text_scale = gui.get(text_node, SCALE_X) local text_scale = gui_get(text_node, SCALE_X)
return text_metrics.width * text_scale return text_metrics.width * text_scale
end end
@@ -28,7 +30,7 @@ end
local function get_icon_width(icon_node) local function get_icon_width(icon_node)
if icon_node then if icon_node then
return gui.get(icon_node, SIZE_X) * gui.get(icon_node, SCALE_X) -- icon width return gui_get(icon_node, SIZE_X) * gui_get(icon_node, SCALE_X) -- icon width
end end
return 0 return 0
@@ -46,39 +48,36 @@ local function get_width(node)
end end
--- Center two nodes. ---Center two nodes.
-- Nodes will be center around 0 x position --Nodes will be center around 0 x position
-- text_node will be first (at left side) --text_node will be first (at left side)
-- @function helper.centrate_text_with_icon ---@param text_node node|nil Gui text node
-- @tparam text|nil text_node Gui text node ---@param icon_node node|nil Gui box node
-- @tparam box|nil icon_node Gui box node ---@param margin number Offset between nodes
-- @tparam number margin Offset between nodes ---@local
-- @local
function M.centrate_text_with_icon(text_node, icon_node, margin) function M.centrate_text_with_icon(text_node, icon_node, margin)
return M.centrate_nodes(margin, text_node, icon_node) return M.centrate_nodes(margin, text_node, icon_node)
end end
--- Center two nodes. ---Center two nodes.
-- Nodes will be center around 0 x position --Nodes will be center around 0 x position
-- icon_node will be first (at left side) --icon_node will be first (at left side)
-- @function helper.centrate_icon_with_text ---@param icon_node node|nil Gui box node
-- @tparam box|nil icon_node Gui box node ---@param text_node node|nil Gui text node
-- @tparam text|nil text_node Gui text node ---@param margin number|nil Offset between nodes
-- @tparam number|nil margin Offset between nodes ---@local
-- @local
function M.centrate_icon_with_text(icon_node, text_node, margin) function M.centrate_icon_with_text(icon_node, text_node, margin)
return M.centrate_nodes(margin, icon_node, text_node) return M.centrate_nodes(margin, icon_node, text_node)
end end
--- Centerate nodes by x position with margin. ---Centerate nodes by x position with margin.
-- ---
-- This functions calculate total width of nodes and set position for each node. ---This functions calculate total width of nodes and set position for each node.
-- The centrate will be around 0 x position. ---The centrate will be around 0 x position.
-- @function helper.centrate_nodes ---@param margin number|nil Offset between nodes
-- @tparam number|nil margin Offset between nodes ---@param ... node Nodes to centrate
-- @param ... Gui nodes
function M.centrate_nodes(margin, ...) function M.centrate_nodes(margin, ...)
margin = margin or 0 margin = margin or 0
@@ -113,36 +112,60 @@ function M.centrate_nodes(margin, ...)
end end
--- Get current screen stretch multiplier for each side ---@param node_id string|node
-- @function helper.get_screen_aspect_koef ---@param template string|nil @Full Path to the template
-- @treturn number stretch_x ---@param nodes table<hash, node>|nil @Nodes what created with gui.clone_tree
-- @treturn number stretch_y ---@return node
function M.get_node(node_id, template, nodes)
if type(node_id) ~= "string" then
-- Assume it's already node from gui.get_node
return node_id
end
-- If template is set, then add it to the node_id
if template and #template > 0 then
node_id = template .. "/" .. node_id
end
-- If nodes is set, then try to find node in it
if nodes then
return nodes[node_id]
end
return gui_get_node(node_id)
end
---Get current screen stretch multiplier for each side
---@return number stretch_x
---@return number stretch_y
function M.get_screen_aspect_koef() function M.get_screen_aspect_koef()
local window_x, window_y = window.get_size() local window_x, window_y = window.get_size()
local stretch_x = window_x / gui.get_width() local stretch_x = window_x / gui.get_width()
local stretch_y = window_y / gui.get_height() local stretch_y = window_y / gui.get_height()
return stretch_x / math.min(stretch_x, stretch_y), local stretch_koef = math.min(stretch_x, stretch_y)
stretch_y / math.min(stretch_x, stretch_y)
local koef_x = window_x / (stretch_koef * sys.get_config_int("display.width"))
local koef_y = window_y / (stretch_koef * sys.get_config_int("display.height"))
return koef_x, koef_y
end end
--- Get current GUI scale for each side ---Get current GUI scale for each side
-- @function helper.get_gui_scale ---@return number scale_x
-- @treturn number scale_x
-- @treturn number scale_y
function M.get_gui_scale() function M.get_gui_scale()
local window_x, window_y = window.get_size() local window_x, window_y = window.get_size()
return math.min(window_x / gui.get_width(), return math.min(window_x / gui.get_width(), window_y / gui.get_height())
window_y / gui.get_height())
end end
--- Move value from current to target value with step amount ---Move value from current to target value with step amount
-- @function helper.step ---@param current number Current value
-- @tparam number current Current value ---@param target number Target value
-- @tparam number target Target value ---@param step number Step amount
-- @tparam number step Step amount ---@return number New value
-- @treturn number New value
function M.step(current, target, step) function M.step(current, target, step)
if current < target then if current < target then
return math.min(current + step, target) return math.min(current + step, target)
@@ -152,43 +175,44 @@ function M.step(current, target, step)
end end
--- Clamp value between min and max ---Clamp value between min and max
-- @function helper.clamp ---@param value number Value
-- @tparam number a Value ---@param v1 number|nil Min value. If nil, value will be clamped to positive infinity
-- @tparam number min Min value ---@param v2 number|nil Max value If nil, value will be clamped to negative infinity
-- @tparam number max Max value ---@return number value Clamped value
-- @treturn number Clamped value function M.clamp(value, v1, v2)
function M.clamp(a, min, max) if v1 and v2 then
if min > max then if v1 > v2 then
min, max = max, min v1, v2 = v2, v1
end
end end
if a >= min and a <= max then if v1 and value < v1 then
return a return v1
elseif a < min then
return min
else
return max
end end
if v2 and value > v2 then
return v2
end
return value
end end
--- Calculate distance between two points ---Calculate distance between two points
-- @function helper.distance ---@param x1 number First point x
-- @tparam number x1 First point x ---@param y1 number First point y
-- @tparam number y1 First point y ---@param x2 number Second point x
-- @tparam number x2 Second point x ---@param y2 number Second point y
-- @tparam number y2 Second point y ---@return number Distance
-- @treturn number Distance
function M.distance(x1, y1, x2, y2) function M.distance(x1, y1, x2, y2)
return math.sqrt((x2 - x1) ^ 2 + (y2 - y1) ^ 2) return math.sqrt((x2 - x1) ^ 2 + (y2 - y1) ^ 2)
end end
--- Return sign of value (-1, 0, 1) ---Return sign of value
-- @function helper.sign ---@param val number Value
-- @tparam number val Value ---@return number sign Sign of value, -1, 0 or 1
-- @treturn number Sign
function M.sign(val) function M.sign(val)
if val == 0 then if val == 0 then
return 0 return 0
@@ -198,47 +222,42 @@ function M.sign(val)
end end
--- Round number to specified decimal places ---Round number to specified decimal places
-- @function helper.round ---@param num number Number
-- @tparam number num Number ---@param num_decimal_places number|nil Decimal places
-- @tparam number|nil num_decimal_places Decimal places ---@return number value Rounded number
-- @treturn number Rounded number
function M.round(num, num_decimal_places) function M.round(num, num_decimal_places)
local mult = 10^(num_decimal_places or 0) local mult = 10^(num_decimal_places or 0)
return math.floor(num * mult + 0.5) / mult return math.floor(num * mult + 0.5) / mult
end end
--- Lerp between two values ---Lerp between two values
-- @function helper.lerp ---@param a number First value
-- @tparam number a First value ---@param b number Second value
-- @tparam number b Second value ---@param t number Lerp amount
-- @tparam number t Lerp amount ---@return number value Lerped value
-- @treturn number Lerped value
function M.lerp(a, b, t) function M.lerp(a, b, t)
return a + (b - a) * t return a + (b - a) * t
end end
--- Check if value is in array and return index of it ---Check if value contains in array
-- @function helper.contains ---@param array any[] Array to check
-- @tparam table t Array ---@param value any Value
-- @param value Value function M.contains(array, value)
-- @treturn number|nil Index of value or nil for index = 1, #array do
function M.contains(t, value) if array[index] == value then
for i = 1, #t do return index
if t[i] == value then
return i
end end
end end
return nil return nil
end end
--- Make a copy table with all nested tables ---Make a copy table with all nested tables
-- @function helper.deepcopy ---@param orig_table table Original table
-- @tparam table orig_table Original table ---@return table Copy of original table
-- @treturn table Copy of original table
function M.deepcopy(orig_table) function M.deepcopy(orig_table)
local orig_type = type(orig_table) local orig_type = type(orig_table)
local copy local copy
@@ -254,11 +273,10 @@ function M.deepcopy(orig_table)
end end
--- Add all elements from source array to the target array ---Add all elements from source array to the target array
-- @function helper.add_array ---@param target any[] Array to put elements from source
-- @tparam any[] target Array to put elements from source ---@param source any[]|nil The source array to get elements from
-- @tparam any[]|nil source The source array to get elements from ---@return any[] The target array
-- @treturn any[] The target array
function M.add_array(target, source) function M.add_array(target, source)
assert(target) assert(target)
@@ -274,37 +292,35 @@ function M.add_array(target, source)
end end
--- Make a check with gui.pick_node, but with additional node_click_area check. ---Make a check with gui.pick_node, but with additional node_click_area check.
-- @function helper.pick_node ---@param node node
-- @tparam node node ---@param x number
-- @tparam number x ---@param y number
-- @tparam number y ---@param node_click_area node|nil
-- @tparam node|nil node_click_area ---@local
-- @local
function M.pick_node(node, x, y, node_click_area) function M.pick_node(node, x, y, node_click_area)
local is_pick = gui.pick_node(node, x, y) local is_pick = gui_pick_node(node, x, y)
if node_click_area then if node_click_area then
is_pick = is_pick and gui.pick_node(node_click_area, x, y) is_pick = is_pick and gui_pick_node(node_click_area, x, y)
end end
return is_pick return is_pick
end end
--- Get node size adjusted by scale
-- @function helper.get_scaled_size ---Get size of node with scale multiplier
-- @tparam node node GUI node ---@param node node GUI node
-- @treturn vector3 Scaled size ---@return vector3 scaled_size
function M.get_scaled_size(node) function M.get_scaled_size(node)
return vmath.mul_per_elem(gui.get_size(node), gui.get_scale(node)) return vmath.mul_per_elem(gui.get_size(node), gui.get_scale(node)) --[[@as vector3]]
end end
--- Get cumulative parent's node scale ---Get cumulative parent's node scale
-- @function helper.get_scene_scale ---@param node node Gui node
-- @tparam node node Gui node ---@param include_passed_node_scale boolean|nil True if add current node scale to result
-- @tparam boolean|nil include_passed_node_scale True if add current node scale to result ---@return vector3 The scene node scale
-- @treturn vector3 The scene node scale
function M.get_scene_scale(node, include_passed_node_scale) function M.get_scene_scale(node, include_passed_node_scale)
local scale = include_passed_node_scale and gui.get_scale(node) or vmath.vector3(1) local scale = include_passed_node_scale and gui.get_scale(node) or vmath.vector3(1)
local parent = gui.get_parent(node) local parent = gui.get_parent(node)
@@ -317,10 +333,9 @@ function M.get_scene_scale(node, include_passed_node_scale)
end end
--- Return closest non inverted clipping parent node for given node ---Return closest non inverted clipping parent node for given node
-- @function helper.get_closest_stencil_node ---@param node node GUI node
-- @tparam node node GUI node ---@return node|nil stencil_node The closest stencil node or nil
-- @treturn node|nil The closest stencil node or nil
function M.get_closest_stencil_node(node) function M.get_closest_stencil_node(node)
if not node then if not node then
return nil return nil
@@ -342,37 +357,35 @@ function M.get_closest_stencil_node(node)
end end
--- Get node offset for given GUI pivot. ---Get pivot offset for given pivot or node
-- ---Offset shown in [-0.5 .. 0.5] range, where -0.5 is left or bottom, 0.5 is right or top.
-- Offset shown in [-0.5 .. 0.5] range, where -0.5 is left or bottom, 0.5 is right or top. ---@param pivot_or_node number|node GUI pivot or node
-- @function helper.get_pivot_offset ---@return vector3 offset The pivot offset
-- @tparam number pivot The gui.PIVOT_* constant function M.get_pivot_offset(pivot_or_node)
-- @treturn vector3 Vector offset with [-0.5..0.5] values if type(pivot_or_node) == "number" then
function M.get_pivot_offset(pivot) return const.PIVOTS[pivot_or_node]
return const.PIVOTS[pivot] end
return const.PIVOTS[gui.get_pivot(pivot_or_node)]
end end
--- Check if device is native mobile (Android or iOS) ---Check if device is native mobile (Android or iOS)
-- @function helper.is_mobile ---@return boolean Is mobile
-- @treturn boolean Is mobile
function M.is_mobile() function M.is_mobile()
return const.CURRENT_SYSTEM_NAME == const.OS.IOS or local sys_name = const.CURRENT_SYSTEM_NAME
const.CURRENT_SYSTEM_NAME == const.OS.ANDROID return sys_name == const.OS.IOS or sys_name == const.OS.ANDROID
end end
--- Check if device is HTML5 ---Check if device is HTML5
-- @function helper.is_web ---@return boolean
-- @treturn boolean Is web
function M.is_web() function M.is_web()
return const.CURRENT_SYSTEM_NAME == const.OS.BROWSER return const.CURRENT_SYSTEM_NAME == const.OS.BROWSER
end end
--- Check if device is HTML5 mobile ---Check if device is HTML5 mobile
-- @function helper.is_web_mobile ---@return boolean
-- @treturn boolean Is web mobile
function M.is_web_mobile() function M.is_web_mobile()
if html5 then if html5 then
return html5.run("(typeof window.orientation !== 'undefined') || (navigator.userAgent.indexOf('IEMobile') !== -1);") == "true" return html5.run("(typeof window.orientation !== 'undefined') || (navigator.userAgent.indexOf('IEMobile') !== -1);") == "true"
@@ -381,18 +394,16 @@ function M.is_web_mobile()
end end
--- Check if device is mobile and can support multitouch ---Check if device is mobile and can support multitouch
-- @function helper.is_multitouch_supported ---@return boolean is_multitouch Is multitouch supported
-- @treturn boolean Is multitouch supported
function M.is_multitouch_supported() function M.is_multitouch_supported()
return M.is_mobile() or M.is_web_mobile() return M.is_mobile() or M.is_web_mobile()
end end
--- Simple table to one-line string converter ---Simple table to one-line string converter
-- @function helper.table_to_string ---@param t table
-- @tparam table t ---@return string
-- @treturn string
function M.table_to_string(t) function M.table_to_string(t)
if not t then if not t then
return "" return ""
@@ -411,11 +422,10 @@ function M.table_to_string(t)
end end
--- Distance from node position to his borders ---Distance from node position to his borders
-- @function helper.get_border ---@param node node GUI node
-- @tparam node node GUI node ---@param offset vector3|nil Offset from node position. Pass current node position to get non relative border values
-- @tparam vector3|nil offset Offset from node position. Pass current node position to get non relative border values ---@return vector4 border Vector4 with border values (left, top, right, down)
-- @treturn vector4 Vector4 with border values (left, top, right, down)
function M.get_border(node, offset) function M.get_border(node, offset)
local pivot = gui.get_pivot(node) local pivot = gui.get_pivot(node)
local pivot_offset = M.get_pivot_offset(pivot) local pivot_offset = M.get_pivot_offset(pivot)
@@ -438,17 +448,9 @@ function M.get_border(node, offset)
end end
--- Get text metric from GUI node. ---Get text metric from GUI node.
-- @function helper.get_text_metrics_from_node ---@param text_node node
-- @tparam node text_node ---@return GUITextMetrics
-- @treturn GUITextMetrics
-- @usage
-- type GUITextMetrics = {
-- width: number,
-- height: number,
-- max_ascent: number,
-- max_descent: number
-- }
function M.get_text_metrics_from_node(text_node) function M.get_text_metrics_from_node(text_node)
local font_resource = gui.get_font_resource(gui.get_font(text_node)) local font_resource = gui.get_font_resource(gui.get_font(text_node))
local options = { local options = {
@@ -466,15 +468,13 @@ function M.get_text_metrics_from_node(text_node)
end end
--- Add value to array with shift policy ---Add value to array with shift policy
-- ---Shift policy can be: left, right, no_shift
-- Shift policy can be: left, right, no_shift ---@param array table Array
-- @function helper.insert_with_shift ---@param item any Item to insert
-- @tparam table array Array ---@param index number|nil Index to insert. If nil, item will be inserted at the end of array
-- @param any Item to insert ---@param shift_policy number|nil The druid_const.SHIFT.* constant
-- @tparam number|nil index Index to insert. If nil, item will be inserted at the end of array ---@return any Inserted item
-- @tparam number|nil shift_policy The druid_const.SHIFT.* constant
-- @treturn any Inserted item
function M.insert_with_shift(array, item, index, shift_policy) function M.insert_with_shift(array, item, index, shift_policy)
shift_policy = shift_policy or const.SHIFT.RIGHT shift_policy = shift_policy or const.SHIFT.RIGHT
@@ -498,14 +498,12 @@ function M.insert_with_shift(array, item, index, shift_policy)
end end
--- Remove value from array with shift policy ---Remove value from array with shift policy
--
-- Shift policy can be: left, right, no_shift -- Shift policy can be: left, right, no_shift
-- @function helper.remove_with_shift ---@param array any[] Array
-- @tparam table array Array ---@param index number|nil Index to remove. If nil, item will be removed from the end of array
-- @tparam number|nil index Index to remove. If nil, item will be removed from the end of array ---@param shift_policy number|nil The druid_const.SHIFT.* constant
-- @tparam number|nil shift_policy The druid_const.SHIFT.* constant ---@return any Removed item
-- @treturn any Removed item
function M.remove_with_shift(array, index, shift_policy) function M.remove_with_shift(array, index, shift_policy)
shift_policy = shift_policy or const.SHIFT.RIGHT shift_policy = shift_policy or const.SHIFT.RIGHT
@@ -530,30 +528,137 @@ function M.remove_with_shift(array, index, shift_policy)
end end
--- Show deprecated message. Once time per message ---Get full position of node in the GUI tree
-- @function helper.deprecated ---@param node node GUI node
-- @tparam string message The deprecated message ---@param root node|nil GUI root node to stop search
-- @local function M.get_full_position(node, root)
local _deprecated_messages = {} local position = gui.get_position(node)
function M.deprecated(message) local parent = gui.get_parent(node)
if _deprecated_messages[message] then while parent and parent ~= root do
return local parent_position = gui.get_position(parent)
position.x = position.x + parent_position.x
position.y = position.y + parent_position.y
parent = gui.get_parent(parent)
end end
print("[Druid]: " .. message) return position
_deprecated_messages[message] = true
end end
--- Show message to require component ---@class druid.animation_data
-- @local ---@field frames table<number, table<string, number>> @List of frames with uv coordinates and size
function M.require_component_message(component_name, component_type) ---@field width number @Width of the animation
component_type = component_type or "extended" ---@field height number @Height of the animation
---@field fps number @Frames per second
---@field current_frame number @Current frame
---@field node node @Node with flipbook animation
---@field v vector4 @Vector with UV coordinates and size
print(string.format("[Druid]: The component %s is %s component. You have to register it via druid.register to use it", component_name, component_type)) ---@param node node
print("[Druid]: Use next code:") ---@param atlas_path string @Path to the atlas
print(string.format('local %s = require("druid.%s.%s")', component_name, component_type, component_name)) ---@return druid.animation_data
print(string.format('druid.register("%s", %s)', component_name, component_name)) function M.get_animation_data_from_node(node, atlas_path)
local atlas_data = resource.get_atlas(atlas_path)
local tex_info = resource.get_texture_info(atlas_data.texture)
local tex_w = tex_info.width
local tex_h = tex_info.height
local animation_data
local sprite_image_id = gui.get_flipbook(node)
for _, animation in ipairs(atlas_data.animations) do
if hash(animation.id) == sprite_image_id then
animation_data = animation
break
end
end
assert(animation_data, "Unable to find image " .. sprite_image_id)
local frames = {}
for index = animation_data.frame_start, animation_data.frame_end - 1 do
local uvs = atlas_data.geometries[index].uvs
assert(#uvs == 8, "Sprite trim mode should be disabled for the images.")
-- UV texture coordinates
-- 1
-- ^ V
-- |
-- |
-- | U
-- 0-------> 1
-- uvs = {
-- 0, 0,
-- 0, height,
-- width, height,
-- width, 0
-- },
-- Point indeces (Point number {uv_index_x, uv_index_y})
-- geometries.indices = {0 (1,2), 1(3,4), 2(5,6), 0(1,2), 2(5,6), 3(7,8)}
-- 1------2
-- | / |
-- | A / |
-- | / B |
-- | / |
-- 0------3
local width = uvs[5] - uvs[1] -- Width of sprite region
local height = uvs[2] - uvs[4] -- Height of sprite region
local is_rotated = height < 0 -- In case of rotated sprite
local x_left = uvs[1]
local y_bottom = uvs[2]
local x_right = uvs[5]
local y_top = uvs[6]
-- Okay now it's correct for non rotated
local uv_coord = vmath.vector4(
x_left / tex_w,
(tex_h - y_bottom) / tex_h,
x_right / tex_w,
(tex_h - y_top) / tex_h
)
if is_rotated then
-- In case the atlas has clockwise rotated sprite.
-- 0---------------1
-- | \ A |
-- | \ |
-- | \ |
-- | B \ |
-- 3---------------2
height = -height
uv_coord.x, uv_coord.y, uv_coord.z, uv_coord.w = uv_coord.y, uv_coord.z, uv_coord.w, uv_coord.x
-- Update uv_coord
--uv_coord = vmath.vector4(
-- u1 / tex_w,
-- (tex_h - v2) / tex_h,
-- u2 / tex_w,
-- (tex_h - v1) / tex_h
--)
end
local frame = {
uv_coord = uv_coord,
w = width,
h = height,
uv_rotated = is_rotated and vmath.vector4(0, 1, 0, 0) or vmath.vector4(1, 0, 0, 0)
}
table.insert(frames, frame)
end
return {
frames = frames,
width = animation_data.width,
height = animation_data.height,
fps = animation_data.fps,
v = vmath.vector4(1, 1, animation_data.width, animation_data.height),
current_frame = 1,
node = node,
}
end end

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 B

View File

@@ -0,0 +1,84 @@
#version 140
uniform sampler2D texture_sampler;
in vec2 var_texcoord0;
in vec4 var_color;
in vec4 var_uv;
in vec4 var_repeat; // [repeat_x, repeat_y, anchor_x, anchor_y]
in vec4 var_params; // [margin_x, margin_y, offset_x, offset_y]
in vec4 var_perspective;
in vec4 var_uv_rotated;
out vec4 color_out;
void main() {
vec2 pivot = var_repeat.zw;
// Margin is a value between 0 and 1 that means offset/padding from the one image to another
vec2 margin = var_params.xy;
vec2 offset = var_params.zw;
vec2 repeat = var_repeat.xy;
// Atlas UV to local UV [0, 1]
float u = (var_texcoord0.x - var_uv.x) / (var_uv.z - var_uv.x);
float v = (var_texcoord0.y - var_uv.y) / (var_uv.w - var_uv.y);
// Adjust local UV by the pivot point. So 0:0 will be at the pivot point of node
u = u - (0.5 + pivot.x);
v = v - (0.5 - pivot.y);
// If rotated, swap UV
if (var_uv_rotated.y < 0.5) {
float temp = u;
u = v;
v = temp;
}
// Adjust repeat by the margin
repeat.x = repeat.x / (1.0 + margin.x);
repeat.y = repeat.y / (1.0 + margin.y);
// Repeat is a value between 0 and 1 that represents the number of times the texture is repeated in the atlas.
float tile_u = fract(u * repeat.x);
float tile_v = fract(v * repeat.y);
float tile_width = 1.0 / repeat.x;
float tile_height = 1.0 / repeat.y;
// Adjust tile UV by the pivot point.
// Not center is left top corner, need to adjust it to pivot point
tile_u = fract(tile_u + pivot.x + 0.5);
tile_v = fract(tile_v - pivot.y + 0.5);
// Apply offset
tile_u = fract(tile_u + offset.x);
tile_v = fract(tile_v + offset.y);
// Extend margins
margin = margin * 0.5;
tile_u = mix(0.0 - margin.x, 1.0 + margin.x, tile_u);
tile_v = mix(0.0 - margin.y, 1.0 + margin.y, tile_v);
float alpha = 0.0;
// If the tile is outside the margins, make it transparent, without IF
alpha = step(0.0, tile_u) * step(tile_u, 1.0) * step(0.0, tile_v) * step(tile_v, 1.0);
tile_u = clamp(tile_u, 0.0, 1.0); // Keep borders in the range 0-1
tile_v = clamp(tile_v, 0.0, 1.0); // Keep borders in the range 0-1
if (var_uv_rotated.y < 0.5) {
float temp = tile_u;
tile_u = tile_v;
tile_v = temp;
}
// Remap local UV to the atlas UV
vec2 uv = vec2(
mix(var_uv.x, var_uv.z, tile_u), // Get texture coordinate from the atlas
mix(var_uv.y, var_uv.w, tile_v) // Get texture coordinate from the atlas
//mix(var_uv.x, var_uv.z, tile_u * var_uv_rotated.x + tile_v * var_uv_rotated.z),
//mix(var_uv.y, var_uv.w, 1.0 - (tile_u * var_uv_rotated.y + tile_v * var_uv_rotated.x))
);
lowp vec4 tex = texture(texture_sampler, uv);
color_out = tex * var_color;
}

View File

@@ -0,0 +1,43 @@
name: "repeat"
tags: "gui"
vertex_program: "/druid/materials/gui_repeat/gui_repeat.vp"
fragment_program: "/druid/materials/gui_repeat/gui_repeat.fp"
vertex_constants {
name: "view_proj"
type: CONSTANT_TYPE_VIEWPROJ
}
vertex_constants {
name: "uv_coord"
type: CONSTANT_TYPE_USER
value {
z: 1.0
w: 1.0
}
}
vertex_constants {
name: "uv_repeat"
type: CONSTANT_TYPE_USER
value {
x: 1.0
y: 1.0
}
}
vertex_constants {
name: "params"
type: CONSTANT_TYPE_USER
value {
}
}
vertex_constants {
name: "perspective"
type: CONSTANT_TYPE_USER
value {
}
}
vertex_constants {
name: "uv_rotated"
type: CONSTANT_TYPE_USER
value {
x: 1.0
}
}

View File

@@ -0,0 +1,50 @@
#version 140
in mediump vec3 position;
in mediump vec2 texcoord0;
in lowp vec4 color;
uniform vertex_inputs
{
highp mat4 view_proj;
highp vec4 uv_coord;
highp vec4 uv_repeat; // [repeat_x, repeat_y, pivot_x, pivot_y]
vec4 uv_rotated;
vec4 params; // [margin_x, margin_y, offset_x, offset_y]
vec4 perspective; // [perspective_x, perspective_y, zoom, offset_y]
};
out mediump vec2 var_texcoord0;
out lowp vec4 var_color;
out highp vec4 var_uv;
out highp vec4 var_repeat;
out vec4 var_params;
out vec4 var_perspective;
out vec4 var_uv_rotated;
void main()
{
var_texcoord0 = texcoord0;
var_color = vec4(color.rgb * color.a, color.a);
var_uv = uv_coord;
var_repeat = uv_repeat;
var_params = params;
var_perspective = perspective;
var_uv_rotated = uv_rotated;
mat4 transform = mat4(
1.0, 0, 0, 0.0,
0, 1.0, 0, 0.0,
0, 0, 1, 0,
0.0, position.z, 0, 1.0
);
// Matrix Info = mat4(
// scale_x, skew_x, 0, offset_x,
// skew_y, scale_y, 0, offset_y,
// 0, 0, scale_z, offset_z,
// perspective_x, perspective_y, perspective_z, zoom
//)
gl_Position = view_proj * vec4(position.xyz, 1.0) * transform;
}

View File

@@ -0,0 +1,10 @@
varying mediump vec2 var_texcoord0;
varying lowp vec4 var_color;
uniform lowp sampler2D texture_sampler;
void main()
{
lowp vec4 tex = texture2D(texture_sampler, var_texcoord0.xy);
gl_FragColor = tex * var_color;
}

View File

@@ -0,0 +1,8 @@
name: "gui_world"
tags: "tile"
vertex_program: "/druid/materials/gui_world/gui_world.vp"
fragment_program: "/druid/materials/gui_world/gui_world.fp"
vertex_constants {
name: "view_proj"
type: CONSTANT_TYPE_VIEWPROJ
}

View File

@@ -0,0 +1,16 @@
uniform highp mat4 view_proj;
// positions are in world space
attribute highp vec3 position;
attribute mediump vec2 texcoord0;
attribute lowp vec4 color;
varying mediump vec2 var_texcoord0;
varying lowp vec4 var_color;
void main()
{
var_texcoord0 = texcoord0;
var_color = vec4(color.rgb * color.a, color.a);
gl_Position = view_proj * vec4(position.xyz, 1.0);
}

View File

@@ -0,0 +1,18 @@
#version 140
uniform sampler2D texture_sampler;
in vec2 var_texcoord0;
in vec4 var_color;
out vec4 color_out;
void main() {
lowp vec4 tex = texture(texture_sampler, var_texcoord0.xy);
if (tex.a < 0.5) {
discard;
}
// Final color of stencil texture
color_out = tex * var_color;
}

View File

@@ -0,0 +1,8 @@
name: "repeat"
tags: "gui"
vertex_program: "/druid/materials/stencil/gui_stencil.vp"
fragment_program: "/druid/materials/stencil/gui_stencil.fp"
vertex_constants {
name: "view_proj"
type: CONSTANT_TYPE_VIEWPROJ
}

View File

@@ -0,0 +1,20 @@
#version 140
uniform vertex_inputs {
highp mat4 view_proj;
};
// positions are in world space
in mediump vec3 position;
in mediump vec2 texcoord0;
in lowp vec4 color;
out mediump vec2 var_texcoord0;
out lowp vec4 var_color;
void main()
{
var_texcoord0 = texcoord0;
var_color = vec4(color.rgb * color.a, color.a);
gl_Position = view_proj * vec4(position.xyz, 1.0);
}

View File

@@ -0,0 +1,18 @@
#version 140
uniform sampler2D texture_sampler;
in vec2 var_texcoord0;
in vec4 var_color;
out vec4 color_out;
void main() {
lowp vec4 tex = texture(texture_sampler, var_texcoord0.xy);
if (tex.a < 0.5) {
discard;
}
// Final color of stencil texture
color_out = tex * var_color;
}

View File

@@ -0,0 +1,8 @@
name: "repeat"
tags: "gui"
vertex_program: "/druid/materials/stencil/gui_stencil.vp"
fragment_program: "/druid/materials/stencil/gui_stencil.fp"
vertex_constants {
name: "view_proj"
type: CONSTANT_TYPE_VIEWPROJ
}

View File

@@ -0,0 +1,20 @@
#version 140
uniform vertex_inputs {
highp mat4 view_proj;
};
// positions are in world space
in mediump vec3 position;
in mediump vec2 texcoord0;
in lowp vec4 color;
out mediump vec2 var_texcoord0;
out lowp vec4 var_color;
void main()
{
var_texcoord0 = texcoord0;
var_color = vec4(color.rgb * color.a, color.a);
gl_Position = view_proj * vec4(position.xyz, 1.0);
}

View File

@@ -1,5 +1,3 @@
-- Copyright (c) 2021 Maksim Tuprikov <insality@gmail.com>. This code is licensed under MIT license
local const = require("druid.const") local const = require("druid.const")
local settings = require("druid.system.settings") local settings = require("druid.system.settings")
@@ -63,7 +61,7 @@ M["hover"] = {
} }
M["drag"] = { M["drag"] = {
DRAG_DEADZONE = 10, -- Size in pixels of drag deadzone DRAG_DEADZONE = 4, -- Size in pixels of drag deadzone
NO_USE_SCREEN_KOEF = false, NO_USE_SCREEN_KOEF = false,
} }

View File

@@ -0,0 +1,29 @@
---@class druid.widget: druid.base_component
---@field druid druid_instance Ready to use druid instance
---@field root node
---@class GUITextMetrics
---@field width number
---@field height number
---@field max_ascent number
---@field max_descent number
---@field offset_x number
---@field offset_y number
---@class utf8
---@field len fun(s: string):number
---@field sub fun(s: string, start_index: number, length: number)
---@field reverse fun()
---@field char fun()
---@field unicode fun()
---@field gensub fun()
---@field byte fun()
---@field find fun()
---@field match fun(s: string, m: string)
---@field gmatch fun(s: string, m: string)
---@field gsub fun()
---@field dump fun()
---@field format fun()
---@field lower fun()
---@field upper fun()
---@field rep fun()

View File

@@ -1,95 +1,24 @@
-- Copyright (c) 2021 Maksim Tuprikov <insality@gmail.com>. This code is licensed under MIT license local events = require("event.events")
local const = require("druid.const")
--- Druid Instance which you use for component creation.
--
-- # Component List #
--
-- For a list of all available components, please refer to the "See Also" section.
--
-- <b># Notes #</b>
--
-- Please review the following API pages:
--
-- @{Helper} - A useful set of functions for working with GUI nodes, such as centering nodes, get GUI scale ratio, etc
--
-- @{DruidEvent} - The core event system in Druid. Learn how to subscribe to any event in every Druid component.
--
-- @{BaseComponent} - The parent class of all Druid components. You can find all default component methods there.
--
-- # Tech Info #
--
-- • To use Druid, you need to create a Druid instance first. This instance is used to spawn components.
--
-- • When using Druid components, provide the node name as a string argument directly. Avoid calling gui.get_node() before passing it to the component. Because Druid can get nodes from template and cloned gui nodes.
--
-- • All Druid and component methods are called using the colon operator (e.g., self.druid:new_button()).
-- @usage
-- local druid = require("druid.druid")
--
-- local function close_window(self)
-- print("Yeah! You closed the game!")
-- end
--
-- function init(self)
-- self.druid = druid.new(self)
--
-- -- Call all druid instance function with ":" syntax:
-- local text = self.druid:new_text("text_header", "Hello Druid!")
-- local button = self.druid:new_button("button_close", close_window)
--
-- -- You not need to save component reference if not need it
-- self.druid:new_back_handler(close_window)
-- end
--
-- @module DruidInstance
-- @alias druid_instance
-- @see BackHandler
-- @see Blocker
-- @see Button
-- @see Checkbox
-- @see CheckboxGroup
-- @see DataList
-- @see Drag
-- @see DynamicGrid
-- @see Hotkey
-- @see Hover
-- @see Input
-- @see LangText
-- @see Layout
-- @see Progress
-- @see RadioGroup
-- @see RichInput
-- @see RichText
-- @see Scroll
-- @see Slider
-- @see StaticGrid
-- @see Swipe
-- @see Text
-- @see Timer
local helper = require("druid.helper") local helper = require("druid.helper")
local settings = require("druid.system.settings") local settings = require("druid.system.settings")
local base_component = require("druid.component") local base_component = require("druid.component")
local drag = require("druid.base.drag") ---@class druid_instance
local text = require("druid.base.text") ---@field components_all druid.base_component[] All created components
local hover = require("druid.base.hover") ---@field components_interest table<string, druid.base_component[]> All components sorted by interest
local scroll = require("druid.base.scroll") ---@field url url
local button = require("druid.base.button") ---@field private _context table Druid context
local blocker = require("druid.base.blocker") ---@field private _style table Druid style table
local static_grid = require("druid.base.static_grid") ---@field private _deleted boolean
local back_handler = require("druid.base.back_handler") ---@field private _is_late_remove_enabled boolean
---@field private _late_remove druid.base_component[]
-- To use this components, you should register them first ---@field private _input_blacklist druid.base_component[]|nil
-- local input = require("druid.extended.input") ---@field private _input_whitelist druid.base_component[]|nil
-- local swipe = require("druid.extended.swipe") ---@field private input_inited boolean
-- local slider = require("druid.extended.slider") ---@field private _late_init_timer_id number
-- local progress = require("druid.extended.progress") ---@field private _input_components druid.base_component[]
-- local data_list = require("druid.extended.data_list") local M = {}
-- local lang_text = require("druid.extended.lang_text")
-- local timer_component = require("druid.extended.timer")
local DruidInstance = {}
local MSG_ADD_FOCUS = hash("acquire_input_focus") local MSG_ADD_FOCUS = hash("acquire_input_focus")
local MSG_REMOVE_FOCUS = hash("release_input_focus") local MSG_REMOVE_FOCUS = hash("release_input_focus")
@@ -106,7 +35,7 @@ end
-- The a and b - two Druid components -- The a and b - two Druid components
-- @local ---@private
local function sort_input_comparator(a, b) local function sort_input_comparator(a, b)
local a_priority = a:get_input_priority() local a_priority = a:get_input_priority()
local b_priority = b:get_input_priority() local b_priority = b:get_input_priority()
@@ -120,7 +49,7 @@ end
local function sort_input_stack(self) local function sort_input_stack(self)
local input_components = self.components_interest[base_component.ON_INPUT] local input_components = self.components_interest[const.ON_INPUT]
if not input_components then if not input_components then
return return
end end
@@ -146,7 +75,55 @@ local function create(self, instance_class)
end end
-- Before processing any input check if we need to update input stack local WIDGET_METATABLE = { __index = base_component }
---Create the Druid component instance
---@param self druid_instance
---@param widget_class druid.base_component
local function create_widget(self, widget_class)
local instance = setmetatable({}, {
__index = setmetatable(widget_class, WIDGET_METATABLE)
})
instance._component = {
_uid = base_component.create_uid(),
name = "Druid Widget",
input_priority = const.PRIORITY_INPUT,
default_input_priority = const.PRIORITY_INPUT,
_is_input_priority_changed = true, -- Default true for sort once time after GUI init
}
instance._meta = {
druid = self,
template = "",
nodes = nil,
context = self._context,
style = nil,
input_enabled = true,
children = {},
parent = type(self._context) ~= "userdata" and self._context,
instance_class = widget_class
}
-- Register
if instance._meta.parent then
instance._meta.parent:__add_child(instance)
end
table.insert(self.components_all, instance)
local register_to = instance:__get_interests()
for i = 1, #register_to do
local interest = register_to[i]
table.insert(self.components_interest[interest], instance)
end
return instance
end
---Before processing any input check if we need to update input stack
---@param self druid_instance
---@param components table[]
local function check_sort_input_stack(self, components) local function check_sort_input_stack(self, components)
if not components or #components == 0 then if not components or #components == 0 then
return return
@@ -168,8 +145,10 @@ local function check_sort_input_stack(self, components)
end end
--- Check whitelists and blacklists for input components ---Check whitelists and blacklists for input components
local function can_use_input_component(self, component) ---@param component druid.base_component
---@return boolean
function M:_can_use_input_component(component)
local can_by_whitelist = true local can_by_whitelist = true
local can_by_blacklist = true local can_by_blacklist = true
@@ -185,13 +164,13 @@ local function can_use_input_component(self, component)
end end
local function process_input(self, action_id, action, components) function M:_process_input(action_id, action, components)
local is_input_consumed = false local is_input_consumed = false
for i = #components, 1, -1 do for i = #components, 1, -1 do
local component = components[i] local component = components[i]
local meta = component._meta local meta = component._meta
if meta.input_enabled and can_use_input_component(self, component) then if meta.input_enabled and self:_can_use_input_component(component) then
if not is_input_consumed then if not is_input_consumed then
is_input_consumed = component:on_input(action_id, action) or false is_input_consumed = component:on_input(action_id, action) or false
else else
@@ -219,36 +198,35 @@ end
--- Druid class constructor --- Druid class constructor
-- @tparam DruidInstance self ---@param context table Druid context. Usually it is self of gui script
-- @tparam table context Druid context. Usually it is self of gui script ---@param style table? Druid style table
-- @tparam table style Druid style table function M:initialize(context, style)
-- @local
function DruidInstance.initialize(self, context, style)
self._context = context self._context = context
self._style = style or settings.default_style self._style = style or settings.default_style
self._deleted = false self._deleted = false
self._is_late_remove_enabled = false self._is_late_remove_enabled = false
self._late_remove = {} self._late_remove = {}
self._is_debug = false
self.url = msg.url()
self._input_blacklist = nil self._input_blacklist = nil
self._input_whitelist = nil self._input_whitelist = nil
self.components_all = {} self.components_all = {}
self.components_interest = {} self.components_interest = {}
for i = 1, #base_component.ALL_INTERESTS do for i = 1, #const.ALL_INTERESTS do
self.components_interest[base_component.ALL_INTERESTS[i]] = {} self.components_interest[const.ALL_INTERESTS[i]] = {}
end end
events.subscribe("druid.window_event", self.on_window_event, self)
events.subscribe("druid.language_change", self.on_language_change, self)
end end
-- Create new component. ---Create new Druid component instance
-- @tparam DruidInstance self ---@generic T: druid.base_component
-- @tparam BaseComponent component Component module ---@param component T
-- @tparam any ... Other component params to pass it to component:init function ---@vararg any
-- @treturn BaseComponent Component instance ---@return T
function DruidInstance.new(self, component, ...) function M:new(component, ...)
local instance = create(self, component) local instance = create(self, component)
if instance.init then if instance.init then
@@ -263,8 +241,7 @@ end
--- Call this in gui_script final function. --- Call this in gui_script final function.
-- @tparam DruidInstance self function M:final()
function DruidInstance.final(self)
local components = self.components_all local components = self.components_all
for i = #components, 1, -1 do for i = #components, 1, -1 do
@@ -276,16 +253,19 @@ function DruidInstance.final(self)
self._deleted = true self._deleted = true
set_input_state(self, false) set_input_state(self, false)
events.unsubscribe("druid.window_event", self.on_window_event, self)
events.unsubscribe("druid.language_change", self.on_language_change, self)
end end
--- Remove created component from Druid instance. --- Remove created component from Druid instance.
-- --
-- Component `on_remove` function will be invoked, if exist. -- Component `on_remove` function will be invoked, if exist.
-- @tparam DruidInstance self ---@generic T: druid.base_component
-- @tparam BaseComponent component Component instance ---@param component T Component instance
-- @treturn boolean True if component was removed ---@return boolean True if component was removed
function DruidInstance.remove(self, component) function M:remove(component)
if self._is_late_remove_enabled then if self._is_late_remove_enabled then
table.insert(self._late_remove, component) table.insert(self._late_remove, component)
return false return false
@@ -334,31 +314,27 @@ end
--- Druid late update function called after initialization and before the regular update step --- Druid late update function called after initialization and before the regular update step
-- This function is used to check the GUI state and perform actions after all components and nodes have been created. -- This function is used to check the GUI state and perform actions after all components and nodes have been created.
-- An example use case is performing an auto stencil check in the GUI hierarchy for input components. -- An example use case is performing an auto stencil check in the GUI hierarchy for input components.
-- @tparam DruidInstance self ---@private
-- @local function M:late_init()
function DruidInstance.late_init(self) local late_init_components = self.components_interest[const.ON_LATE_INIT]
local late_init_components = self.components_interest[base_component.ON_LATE_INIT]
while late_init_components[1] do while late_init_components[1] do
late_init_components[1]:on_late_init() late_init_components[1]:on_late_init()
table.remove(late_init_components, 1) table.remove(late_init_components, 1)
end end
if not self.input_inited and #self.components_interest[base_component.ON_INPUT] > 0 then if not self.input_inited and #self.components_interest[const.ON_INPUT] > 0 then
-- Input init on late init step, to be sure it goes after user go acquire input -- Input init on late init step, to be sure it goes after user go acquire input
set_input_state(self, true) set_input_state(self, true)
end end
end end
--- Call this in gui_script update function. ---Call this in gui_script update function.
-- ---@param dt number Delta time
-- Used for: scroll, progress, timer components function M:update(dt)
-- @tparam DruidInstance self
-- @tparam number dt Delta time
function DruidInstance.update(self, dt)
self._is_late_remove_enabled = true self._is_late_remove_enabled = true
local components = self.components_interest[base_component.ON_UPDATE] local components = self.components_interest[const.ON_UPDATE]
for i = 1, #components do for i = 1, #components do
components[i]:update(dt) components[i]:update(dt)
end end
@@ -368,19 +344,16 @@ function DruidInstance.update(self, dt)
end end
--- Call this in gui_script on_input function. ---Call this in gui_script on_input function.
-- ---@param action_id hash Action_id from on_input
-- Used for almost all components ---@param action table Action from on_input
-- @tparam DruidInstance self ---@return boolean The boolean value is input was consumed
-- @tparam hash action_id Action_id from on_input function M:on_input(action_id, action)
-- @tparam table action Action from on_input
-- @treturn boolean The boolean value is input was consumed
function DruidInstance.on_input(self, action_id, action)
self._is_late_remove_enabled = true self._is_late_remove_enabled = true
local components = self.components_interest[base_component.ON_INPUT] local components = self.components_interest[const.ON_INPUT]
check_sort_input_stack(self, components) check_sort_input_stack(self, components)
local is_input_consumed = process_input(self, action_id, action, components) local is_input_consumed = self:_process_input(action_id, action, components)
self._is_late_remove_enabled = false self._is_late_remove_enabled = false
self:_clear_late_remove() self:_clear_late_remove()
@@ -390,38 +363,19 @@ end
--- Call this in gui_script on_message function. --- Call this in gui_script on_message function.
-- ---@param message_id hash Message_id from on_message
-- Used for special actions. See SPECIFIC_UI_MESSAGES table ---@param message table Message from on_message
-- @tparam DruidInstance self ---@param sender url Sender from on_message
-- @tparam hash message_id Message_id from on_message function M:on_message(message_id, message, sender)
-- @tparam table message Message from on_message if message_id == const.MSG_LAYOUT_CHANGED then
-- @tparam url sender Sender from on_message
function DruidInstance.on_message(self, message_id, message, sender)
local specific_ui_message = base_component.SPECIFIC_UI_MESSAGES[message_id]
if specific_ui_message == base_component.ON_MESSAGE_INPUT then
-- ON_MESSAGE_INPUT is special message, need to perform additional logic
local components = self.components_interest[base_component.ON_MESSAGE_INPUT]
if components then
for i = 1, #components do
local component = components[i]
if can_use_input_component(self, component) then
component[specific_ui_message](component, hash(message.node_id), message)
end
end
end
elseif specific_ui_message then
-- Resend special message to all components with the related interest -- Resend special message to all components with the related interest
local components = self.components_interest[specific_ui_message] local components = self.components_interest[const.ON_LAYOUT_CHANGE]
if components then
for i = 1, #components do for i = 1, #components do
local component = components[i] components[i]:on_layout_change()
component[specific_ui_message](component, message, sender)
end
end end
else else
-- Resend message to all components with on_message interest -- Resend message to all components with on_message interest
local components = self.components_interest[base_component.ON_MESSAGE] local components = self.components_interest[const.ON_MESSAGE]
for i = 1, #components do for i = 1, #components do
components[i]:on_message(message_id, message, sender) components[i]:on_message(message_id, message, sender)
end end
@@ -429,51 +383,44 @@ function DruidInstance.on_message(self, message_id, message, sender)
end end
--- Calls the on_focus_lost function in all related components function M:on_window_event(window_event)
-- This one called by on_window_callback by global window listener if window_event == window.WINDOW_EVENT_FOCUS_LOST then
-- @tparam DruidInstance self local components = self.components_interest[const.ON_FOCUS_LOST]
-- @local
function DruidInstance.on_focus_lost(self)
local components = self.components_interest[base_component.ON_FOCUS_LOST]
for i = 1, #components do for i = 1, #components do
components[i]:on_focus_lost() components[i]:on_focus_lost()
end end
end elseif window_event == window.WINDOW_EVENT_FOCUS_GAINED then
local components = self.components_interest[const.ON_FOCUS_GAINED]
--- Calls the on_focus_gained function in all related components
-- This one called by on_window_callback by global window listener
-- @tparam DruidInstance self
-- @local
function DruidInstance.on_focus_gained(self)
local components = self.components_interest[base_component.ON_FOCUS_GAINED]
for i = 1, #components do for i = 1, #components do
components[i]:on_focus_gained() components[i]:on_focus_gained()
end end
elseif window_event == window.WINDOW_EVENT_RESIZED then
local components = self.components_interest[const.ON_WINDOW_RESIZED]
for i = 1, #components do
components[i]:on_window_resized()
end
end
end end
--- Calls the on_language_change function in all related components --- Calls the on_language_change function in all related components
-- This one called by global druid.on_language_change, but can be -- This one called by global druid.on_language_change, but can be
-- call manualy to update all translations -- call manualy to update all translations
-- @tparam DruidInstance self ---@private
-- @local function M:on_language_change()
function DruidInstance.on_language_change(self) local components = self.components_interest[const.ON_LANGUAGE_CHANGE]
local components = self.components_interest[base_component.ON_LANGUAGE_CHANGE]
for i = 1, #components do for i = 1, #components do
components[i]:on_language_change() components[i]:on_language_change()
end end
end end
--- Set whitelist components for input processing. ---Set whitelist components for input processing.
-- ---If whitelist is not empty and component not contains in this list,
-- If whitelist is not empty and component not contains in this list, ---component will be not processed on input step
-- component will be not processed on input step ---@param whitelist_components table|druid.base_component[] The array of component to whitelist
-- @tparam DruidInstance self ---@return druid_instance
-- @tparam table|BaseComponent|nil whitelist_components The array of component to whitelist function M:set_whitelist(whitelist_components)
-- @treturn self @{DruidInstance}
function DruidInstance.set_whitelist(self, whitelist_components)
if whitelist_components and whitelist_components._component then if whitelist_components and whitelist_components._component then
whitelist_components = { whitelist_components } whitelist_components = { whitelist_components }
end end
@@ -488,14 +435,12 @@ function DruidInstance.set_whitelist(self, whitelist_components)
end end
--- Set blacklist components for input processing. ---Set blacklist components for input processing.
-- ---If blacklist is not empty and component contains in this list,
-- If blacklist is not empty and component contains in this list, ---component will be not processed on input step DruidInstance
-- component will be not processed on input step ---@param blacklist_components table|druid.base_component[] The array of component to blacklist
-- @tparam DruidInstance self @{DruidInstance} ---@return druid_instance
-- @tparam table|BaseComponent|nil blacklist_components The array of component to blacklist function M:set_blacklist(blacklist_components)
-- @treturn self @{DruidInstance}
function DruidInstance.set_blacklist(self, blacklist_components)
if blacklist_components and blacklist_components._component then if blacklist_components and blacklist_components._component then
blacklist_components = { blacklist_components } blacklist_components = { blacklist_components }
end end
@@ -510,35 +455,9 @@ function DruidInstance.set_blacklist(self, blacklist_components)
end end
--- Set debug mode for current Druid instance. It's enable debug log messages --- Remove all components on late remove step DruidInstance
-- @tparam DruidInstance self @{DruidInstance} ---@private
-- @tparam boolean|nil is_debug function M:_clear_late_remove()
-- @treturn self @{DruidInstance}
-- @local
function DruidInstance.set_debug(self, is_debug)
self._is_debug = is_debug
return self
end
--- Log message, if is_debug mode is enabled
-- @tparam DruidInstance self @{DruidInstance}
-- @tparam string message
-- @tparam table|nil context
-- @local
function DruidInstance.log_message(self, message, context)
if not self._is_debug then
return
end
print("[Druid]:", message, helper.table_to_string(context))
end
--- Remove all components on late remove step
-- @tparam DruidInstance self @{DruidInstance}
-- @local
function DruidInstance._clear_late_remove(self)
if #self._late_remove == 0 then if #self._late_remove == 0 then
return return
end end
@@ -550,229 +469,245 @@ function DruidInstance._clear_late_remove(self)
end end
--- Create @{Button} component ---Create new Druid widget instance
-- @tparam DruidInstance self ---@generic T: druid.base_component
-- @tparam string|node node The node_id or gui.get_node(node_id) ---@param widget T
-- @tparam function|nil callback Button callback ---@param template string|nil The template name used by widget
-- @tparam any|nil params Button callback params ---@param nodes table<hash, node>|node|nil The nodes table from gui.clone_tree or prefab node to use for clone
-- @tparam node|string|nil anim_node Button anim node (node, if not provided) ---@vararg any
-- @treturn Button @{Button} component ---@return T
function DruidInstance.new_button(self, node, callback, params, anim_node) function M:new_widget(widget, template, nodes, ...)
return DruidInstance.new(self, button, node, callback, params, anim_node) local instance = create_widget(self, widget)
if type(nodes) == "userdata" then
nodes = gui.clone_tree(nodes) --[[@as table<hash, node>]]
end
instance.druid = instance:get_druid(template, nodes)
if instance.init then
instance:init(...)
end
if instance.on_late_init or (not self.input_inited and instance.on_input) then
schedule_late_init(self)
end
return instance
end end
--- Create @{Blocker} component local button = require("druid.base.button")
-- @tparam DruidInstance self ---Create Button component
-- @tparam string|node node The node_id or gui.get_node(node_id) ---@param node string|node The node_id or gui.get_node(node_id)
-- @treturn Blocker @{Blocker} component ---@param callback function|nil Button callback
function DruidInstance.new_blocker(self, node) ---@param params any|nil Button callback params
return DruidInstance.new(self, blocker, node) ---@param anim_node node|string|nil Button anim node (node, if not provided)
---@return druid.button Button component
function M:new_button(node, callback, params, anim_node)
return self:new(button, node, callback, params, anim_node)
end end
--- Create @{BackHandler} component local blocker = require("druid.base.blocker")
-- @tparam DruidInstance self ---Create Blocker component
-- @tparam function|nil callback @The callback(self, custom_args) to call on back event ---@param node string|node The node_id or gui.get_node(node_id)
-- @tparam any|nil params Callback argument ---@return druid.blocker component Blocker component
-- @treturn BackHandler @{BackHandler} component function M:new_blocker(node)
function DruidInstance.new_back_handler(self, callback, params) return self:new(blocker, node)
return DruidInstance.new(self, back_handler, callback, params)
end end
--- Create @{Hover} component local back_handler = require("druid.base.back_handler")
-- @tparam DruidInstance self ---Create BackHandler component
-- @tparam string|node node The node_id or gui.get_node(node_id) ---@param callback function|nil The callback(self, custom_args) to call on back event
-- @tparam function|nil on_hover_callback Hover callback ---@param params any|nil Callback argument
-- @tparam function|nil on_mouse_hover_callback Mouse hover callback ---@return druid.back_handler component BackHandler component
-- @treturn Hover @{Hover} component function M:new_back_handler(callback, params)
function DruidInstance.new_hover(self, node, on_hover_callback, on_mouse_hover_callback) return self:new(back_handler, callback, params)
return DruidInstance.new(self, hover, node, on_hover_callback, on_mouse_hover_callback)
end end
--- Create @{Text} component local hover = require("druid.base.hover")
-- @tparam DruidInstance self ---Create Hover component
-- @tparam string|node node The node_id or gui.get_node(node_id) ---@param node string|node The node_id or gui.get_node(node_id)
-- @tparam string|nil value Initial text. Default value is node text from GUI scene. ---@param on_hover_callback function|nil Hover callback
-- @tparam boolean|nil no_adjust If true, text will be not auto-adjust size ---@param on_mouse_hover_callback function|nil Mouse hover callback
-- @treturn Text @{Text} component ---@return druid.hover component Hover component
function DruidInstance.new_text(self, node, value, no_adjust) function M:new_hover(node, on_hover_callback, on_mouse_hover_callback)
return DruidInstance.new(self, text, node, value, no_adjust) return self:new(hover, node, on_hover_callback, on_mouse_hover_callback)
end end
--- Create @{StaticGrid} component local text = require("druid.base.text")
-- @tparam DruidInstance self ---Create Text component
-- @tparam string|node parent_node The node_id or gui.get_node(node_id). Parent of all Grid items. ---@param node string|node The node_id or gui.get_node(node_id)
-- @tparam node item Element prefab. Required to get grid's item size. Can be adjusted separately. ---@param value string|nil Initial text. Default value is node text from GUI scene.
-- @tparam number|nil in_row How many nodes in row can be placed ---@param adjust_type string|nil Adjust type for text. By default is DOWNSCALE. Look const.TEXT_ADJUST for reference
-- @treturn StaticGrid @{StaticGrid} component ---@return druid.text component Text component
-- @local function M:new_text(node, value, adjust_type)
function DruidInstance.new_grid(self, parent_node, item, in_row) return self:new(text, node, value, adjust_type)
return DruidInstance.new(self, static_grid, parent_node, item, in_row)
end end
--- Create @{StaticGrid} component local static_grid = require("druid.base.static_grid")
-- @tparam DruidInstance self ---Create Grid component
-- @tparam string|node parent_node The node_id or gui.get_node(node_id). Parent of all Grid items. ---@param parent_node string|node The node_id or gui.get_node(node_id). Parent of all Grid items.
-- @tparam string|node item Item prefab. Required to get grid's item size. Can be adjusted separately. ---@param item string|node Item prefab. Required to get grid's item size. Can be adjusted separately.
-- @tparam number|nil in_row How many nodes in row can be placed ---@param in_row number|nil How many nodes in row can be placed
-- @treturn StaticGrid @{StaticGrid} component ---@return druid.grid component Grid component
function DruidInstance.new_static_grid(self, parent_node, item, in_row) function M:new_grid(parent_node, item, in_row)
return DruidInstance.new(self, static_grid, parent_node, item, in_row) return self:new(static_grid, parent_node, item, in_row)
end end
--- Create @{Scroll} component local scroll = require("druid.base.scroll")
-- @tparam DruidInstance self ---Create Scroll component
-- @tparam string|node view_node The node_id or gui.get_node(node_id). Will used as user input node. ---@param view_node string|node The node_id or gui.get_node(node_id). Will used as user input node.
-- @tparam string|node content_node The node_id or gui.get_node(node_id). Will used as scrollable node inside view_node. ---@param content_node string|node The node_id or gui.get_node(node_id). Will used as scrollable node inside view_node.
-- @treturn Scroll @{Scroll} component ---@return druid.scroll component Scroll component
function DruidInstance.new_scroll(self, view_node, content_node) function M:new_scroll(view_node, content_node)
return DruidInstance.new(self, scroll, view_node, content_node) return self:new(scroll, view_node, content_node)
end end
--- Create @{Drag} component local drag = require("druid.base.drag")
-- @tparam DruidInstance self ---Create Drag component
-- @tparam string|node node The node_id or gui.get_node(node_id). Will used as user input node. ---@param node string|node The node_id or gui.get_node(node_id). Will used as user input node.
-- @tparam function|nil on_drag_callback Callback for on_drag_event(self, dx, dy) ---@param on_drag_callback function|nil Callback for on_drag_event(self, dx, dy)
-- @treturn Drag @{Drag} component ---@return druid.drag component Drag component
function DruidInstance.new_drag(self, node, on_drag_callback) function M:new_drag(node, on_drag_callback)
return DruidInstance.new(self, drag, node, on_drag_callback) return self:new(drag, node, on_drag_callback)
end end
--- Create @{Swipe} component local swipe = require("druid.extended.swipe")
-- @tparam DruidInstance self ---Create Swipe component
-- @tparam string|node node The node_id or gui.get_node(node_id). Will used as user input node. ---@param node string|node The node_id or gui.get_node(node_id). Will used as user input node.
-- @tparam function|nil on_swipe_callback Swipe callback for on_swipe_end event ---@param on_swipe_callback function|nil Swipe callback for on_swipe_end event
-- @treturn Swipe @{Swipe} component ---@return druid.swipe component Swipe component
function DruidInstance.new_swipe(self, node, on_swipe_callback) function M:new_swipe(node, on_swipe_callback)
return helper.require_component_message("swipe") return self:new(swipe, node, on_swipe_callback)
end end
--- Create @{DynamicGrid} component local lang_text = require("druid.extended.lang_text")
-- Deprecated ---Create LangText component
-- @tparam DruidInstance self ---@param node string|node The_node id or gui.get_node(node_id)
-- @tparam string|node parent_node The node_id or gui.get_node(node_id). Parent of all Grid items. ---@param locale_id string|nil Default locale id or text from node as default
-- @treturn DynamicGrid @{DynamicGrid} component ---@param adjust_type string|nil Adjust type for text node. Default: const.TEXT_ADJUST.DOWNSCALE
function DruidInstance.new_dynamic_grid(self, parent_node) ---@return druid.lang_text component LangText component
return helper.require_component_message("dynamic_grid") function M:new_lang_text(node, locale_id, adjust_type)
return self:new(lang_text, node, locale_id, adjust_type)
end end
--- Create @{LangText} component local slider = require("druid.extended.slider")
-- @tparam DruidInstance self ---Create Slider component
-- @tparam string|node node The_node id or gui.get_node(node_id) ---@param pin_node string|node The_node id or gui.get_node(node_id).
-- @tparam string|nil locale_id Default locale id or text from node as default ---@param end_pos vector3 The end position of slider
-- @tparam string|nil adjust_type Adjust type for text node. Default: const.TEXT_ADJUST.DOWNSCALE ---@param callback function|nil On slider change callback
-- @treturn LangText @{LangText} component ---@return druid.slider component Slider component
function DruidInstance.new_lang_text(self, node, locale_id, adjust_type) function M:new_slider(pin_node, end_pos, callback)
return helper.require_component_message("lang_text") return self:new(slider, pin_node, end_pos, callback)
end end
--- Create @{Slider} component local input = require("druid.extended.input")
-- @tparam DruidInstance self ---Create Input component
-- @tparam string|node pin_node The_node id or gui.get_node(node_id). ---@param click_node string|node Button node to enabled input component
-- @tparam vector3 end_pos The end position of slider ---@param text_node string|node|druid.text Text node what will be changed on user input
-- @tparam function|nil callback On slider change callback ---@param keyboard_type number|nil Gui keyboard type for input field
-- @treturn Slider @{Slider} component ---@return druid.input component Input component
function DruidInstance.new_slider(self, pin_node, end_pos, callback) function M:new_input(click_node, text_node, keyboard_type)
return helper.require_component_message("slider") return self:new(input, click_node, text_node, keyboard_type)
end end
--- Create @{Input} component local data_list = require("druid.extended.data_list")
-- @tparam DruidInstance self ---Create DataList component
-- @tparam string|node click_node Button node to enabled input component ---@param druid_scroll druid.scroll The Scroll instance for Data List component
-- @tparam string|node|druid.text text_node Text node what will be changed on user input ---@param druid_grid druid.grid The StaticGrid} or @{DynamicGrid instance for Data List component
-- @tparam number|nil keyboard_type Gui keyboard type for input field ---@param create_function function The create function callback(self, data, index, data_list). Function should return (node, [component])
-- @treturn Input @{Input} component ---@return druid.data_list component DataList component
function DruidInstance.new_input(self, click_node, text_node, keyboard_type) function M:new_data_list(druid_scroll, druid_grid, create_function)
return helper.require_component_message("input") return self:new(data_list, druid_scroll, druid_grid, create_function)
end end
--- Create @{DataList} component local timer_component = require("druid.extended.timer")
-- @tparam DruidInstance self ---Create Timer component
-- @tparam Scroll druid_scroll The Scroll instance for Data List component ---@param node string|node Gui text node
-- @tparam StaticGrid druid_grid The @{StaticGrid} or @{DynamicGrid} instance for Data List component ---@param seconds_from number|nil Start timer value in seconds
-- @tparam function create_function The create function callback(self, data, index, data_list). Function should return (node, [component]) ---@param seconds_to number|nil End timer value in seconds
-- @treturn DataList @{DataList} component ---@param callback function|nil Function on timer end
function DruidInstance.new_data_list(self, druid_scroll, druid_grid, create_function) ---@return druid.timer component Timer component
return helper.require_component_message("data_list") function M:new_timer(node, seconds_from, seconds_to, callback)
return self:new(timer_component, node, seconds_from, seconds_to, callback)
end end
--- Create @{Timer} component local progress = require("druid.extended.progress")
-- @tparam DruidInstance self ---Create Progress component
-- @tparam string|node node Gui text node ---@param node string|node Progress bar fill node or node name
-- @tparam number seconds_from Start timer value in seconds ---@param key string Progress bar direction: const.SIDE.X or const.SIDE.Y
-- @tparam number|nil seconds_to End timer value in seconds ---@param init_value number|nil Initial value of progress bar. Default: 1
-- @tparam function|nil callback Function on timer end ---@return druid.progress component Progress component
-- @treturn Timer @{Timer} component function M:new_progress(node, key, init_value)
function DruidInstance.new_timer(self, node, seconds_from, seconds_to, callback) return self:new(progress, node, key, init_value)
return helper.require_component_message("timer")
end end
--- Create @{Progress} component local layout = require("druid.extended.layout")
-- @tparam DruidInstance self ---Create Layout component
-- @tparam string|node node Progress bar fill node or node name ---@param node string|node The_node id or gui.get_node(node_id).
-- @tparam string key Progress bar direction: const.SIDE.X or const.SIDE.Y ---@param mode string|nil vertical|horizontal|horizontal_wrap. Default: horizontal
-- @tparam number|nil init_value Initial value of progress bar. Default: 1 ---@return druid.layout component Layout component
-- @treturn Progress @{Progress} component function M:new_layout(node, mode)
function DruidInstance.new_progress(self, node, key, init_value) return self:new(layout, node, mode)
return helper.require_component_message("progress")
end end
--- Create @{Layout} component local container = require("druid.extended.container")
-- @tparam DruidInstance self ---Create Container component
-- @tparam string|node node The_node id or gui.get_node(node_id). ---@param node string|node The_node id or gui.get_node(node_id).
-- @tparam string mode The layout mode ---@param mode string|nil Layout mode
-- @treturn Layout @{Layout} component ---@param callback fun(self: druid.container, size: vector3)|nil Callback on size changed
function DruidInstance.new_layout(self, node, mode) ---@return druid.container container component
return helper.require_component_message("layout") function M:new_container(node, mode, callback)
return self:new(container, node, mode, callback)
end end
--- Create @{Hotkey} component local hotkey = require("druid.extended.hotkey")
-- @tparam DruidInstance self ---Create Hotkey component
-- @tparam string|string[] keys_array Keys for trigger action. Should contains one action key and any amount of modificator keys ---@param keys_array string|string[] Keys for trigger action. Should contains one action key and any amount of modificator keys
-- @tparam function callback The callback function ---@param callback function|nil The callback function
-- @tparam any|nil callback_argument The argument to pass into the callback function ---@param callback_argument any|nil The argument to pass into the callback function
-- @treturn Hotkey @{Hotkey} component ---@return druid.hotkey component Hotkey component
function DruidInstance.new_hotkey(self, keys_array, callback, callback_argument) function M:new_hotkey(keys_array, callback, callback_argument)
return helper.require_component_message("hotkey") return self:new(hotkey, keys_array, callback, callback_argument)
end end
--- Create @{RichText} component. local rich_text = require("druid.custom.rich_text.rich_text")
-- @tparam DruidInstance self ---Create RichText component.
-- @tparam string|node text_node The text node to make Rich Text ---@param text_node string|node The text node to make Rich Text
-- @tparam string|nil value The initial text value. Default will be gui.get_text(text_node) ---@param value string|nil The initial text value. Default will be gui.get_text(text_node)
-- @treturn RichText @{RichText} component ---@return druid.rich_text component RichText component
function DruidInstance.new_rich_text(self, text_node, value) function M:new_rich_text(text_node, value)
return helper.require_component_message("rich_text", "custom") return self:new(rich_text, text_node, value)
end end
--- Create @{RichInput} component. local rich_input = require("druid.custom.rich_input.rich_input")
---Create RichInput component.
-- As a template please check rich_input.gui layout. -- As a template please check rich_input.gui layout.
-- @tparam DruidInstance self ---@param template string The template string name
-- @tparam string template The template string name ---@param nodes table|nil Nodes table from gui.clone_tree
-- @tparam table nodes Nodes table from gui.clone_tree ---@return druid.rich_input component RichInput component
-- @treturn RichInput @{RichInput} component function M:new_rich_input(template, nodes)
function DruidInstance.new_rich_input(self, template, nodes) return self:new(rich_input, template, nodes)
return helper.require_component_message("rich_input", "custom")
end end
return DruidInstance return M

View File

@@ -1,21 +1,15 @@
-- Copyright (c) 2021 Maksim Tuprikov <insality@gmail.com>. This code is licensed under MIT license ---@class druid.system.settings
--- Druid settings file
-- @module settings
-- @local
local M = {} local M = {}
M.default_style = nil M.default_style = nil
---@param text_id string
function M.get_text(name, a, b, c, d, e, f, g) ---@vararg any
function M.get_text(text_id, ...)
return "[Druid]: locales not inited" return "[Druid]: locales not inited"
end end
function M.play_sound(sound_id)
function M.play_sound(name)
end end
return M return M

View File

@@ -1,21 +1,16 @@
local component = require("druid.component") ---@class widget.TEMPLATE: druid.widget
local M = {}
---@class component_name : druid.base_component
local Component = component.create("component_name")
-- Component constructor. Template name and nodes are optional. Pass it if you use it in your component function M:init()
function Component:init(template, nodes)
self.druid = self:get_druid(template, nodes)
self.root = self:get_node("root") self.root = self:get_node("root")
self.button = self.druid:new_button("button", self.on_button, self)
self.button = self.druid:new_button("button", function() end)
end end
-- [OPTIONAL] Call on component remove or on druid:final function M:on_button()
function Component:on_remove() print("Root node", self.root)
end end
return Component return M

View File

@@ -1,10 +1,10 @@
local component = require("druid.component") local component = require("druid.component")
---@class component_name : druid.base_component ---@class new_component: druid.base_component
local Component = component.create("component_name") local M = component.create("new_component")
-- Component constructor. Template name and nodes are optional. Pass it if you use it in your component -- Component constructor. Template name and nodes are optional. Pass it if you use it in your component
function Component:init(template, nodes) function M:init(template, nodes)
-- If your component is gui template, pass the template name and set it -- If your component is gui template, pass the template name and set it
-- If your component is cloned my gui.clone_tree, pass nodes to component and set it -- If your component is cloned my gui.clone_tree, pass nodes to component and set it
-- Use inner druid instance to create components inside this component -- Use inner druid instance to create components inside this component
@@ -17,55 +17,55 @@ end
-- [OPTIONAL] Call every update step -- [OPTIONAL] Call every update step
function Component:update(dt) function M:update(dt)
end end
-- [OPTIONAL] Call default on_input from gui script -- [OPTIONAL] Call default on_input from gui script
function Component:on_input(action_id, action) function M:on_input(action_id, action)
return false return false
end end
-- [OPTIONAL] Call on component creation and on component:set_style() function -- [OPTIONAL] Call on component creation and on component:set_style() function
function Component:on_style_change(style) function M:on_style_change(style)
end end
-- [OPTIONAL] Call default on_message from gui script -- [OPTIONAL] Call default on_message from gui script
function Component:on_message(message_id, message, sender) function M:on_message(message_id, message, sender)
end end
-- [OPTIONAL] Call if druid has triggered on_language_change -- [OPTIONAL] Call if druid has triggered on_language_change
function Component:on_language_change() function M:on_language_change()
end end
-- [OPTIONAL] Call if game layout has changed and need to restore values in component -- [OPTIONAL] Call if game layout has changed and need to restore values in component
function Component:on_layout_change() function M:on_layout_change()
end end
-- [OPTIONAL] Call, if input was capturing before this component -- [OPTIONAL] Call, if input was capturing before this component
-- Example: scroll is start scrolling, so you need unhover button -- Example: scroll is start scrolling, so you need unhover button
function Component:on_input_interrupt() function M:on_input_interrupt()
end end
-- [OPTIONAL] Call, if game lost focus -- [OPTIONAL] Call, if game lost focus
function Component:on_focus_lost() function M:on_focus_lost()
end end
-- [OPTIONAL] Call, if game gained focus -- [OPTIONAL] Call, if game gained focus
function Component:on_focus_gained() function M:on_focus_gained()
end end
-- [OPTIONAL] Call on component remove or on druid:final -- [OPTIONAL] Call on component remove or on druid:final
function Component:on_remove() function M:on_remove()
end end
return Component return M

View File

@@ -0,0 +1,222 @@
fonts {
name: "druid_text_regular"
font: "/druid/fonts/druid_text_regular.font"
}
fonts {
name: "druid_text_bold"
font: "/druid/fonts/druid_text_bold.font"
}
textures {
name: "druid"
texture: "/druid/druid.atlas"
}
nodes {
size {
x: 200.0
y: 140.0
}
type: TYPE_BOX
id: "root"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
visible: false
}
nodes {
type: TYPE_TEMPLATE
id: "mini_graph"
parent: "root"
inherit_alpha: true
template: "/druid/widget/mini_graph/mini_graph.gui"
}
nodes {
color {
x: 0.173
y: 0.184
z: 0.204
}
type: TYPE_BOX
id: "mini_graph/root"
parent: "mini_graph"
overridden_fields: 5
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "mini_graph/header"
parent: "mini_graph/root"
template_node_child: true
}
nodes {
type: TYPE_TEXT
text: "FPS"
id: "mini_graph/text_header"
parent: "mini_graph/header"
overridden_fields: 8
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "mini_graph/icon_drag"
parent: "mini_graph/header"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "mini_graph/content"
parent: "mini_graph/root"
template_node_child: true
}
nodes {
color {
x: 0.525
y: 0.525
z: 0.525
}
type: TYPE_BOX
id: "mini_graph/prefab_line"
parent: "mini_graph/content"
overridden_fields: 5
template_node_child: true
}
nodes {
color {
x: 0.957
y: 0.608
z: 0.608
}
type: TYPE_BOX
id: "mini_graph/color_low"
parent: "mini_graph/content"
overridden_fields: 5
template_node_child: true
}
nodes {
size {
x: 200.0
y: 100.0
}
type: TYPE_BOX
id: "content"
parent: "root"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
visible: false
}
nodes {
position {
x: -96.0
y: 12.0
}
scale {
x: 0.3
y: 0.3
}
size {
x: 260.0
y: 40.0
}
color {
x: 0.463
y: 0.475
z: 0.49
}
type: TYPE_TEXT
text: "12 FPS"
font: "druid_text_regular"
id: "text_min_fps"
pivot: PIVOT_W
outline {
x: 1.0
y: 1.0
z: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
}
parent: "content"
inherit_alpha: true
outline_alpha: 0.0
shadow_alpha: 0.0
}
nodes {
position {
y: 12.0
}
scale {
x: 0.3
y: 0.3
}
size {
x: 260.0
y: 40.0
}
color {
x: 0.463
y: 0.475
z: 0.49
}
type: TYPE_TEXT
text: "60 FPS"
font: "druid_text_bold"
id: "text_fps"
outline {
x: 1.0
y: 1.0
z: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
}
parent: "content"
inherit_alpha: true
outline_alpha: 0.0
shadow_alpha: 0.0
}
nodes {
position {
x: -33.4
y: 30.0
}
size {
x: 3.0
y: 8.0
}
color {
x: 0.173
y: 0.184
z: 0.204
}
type: TYPE_BOX
texture: "druid/pixel"
id: "line_second_1"
pivot: PIVOT_N
parent: "content"
inherit_alpha: true
}
nodes {
position {
x: 33.2
y: 30.0
}
size {
x: 3.0
y: 8.0
}
color {
x: 0.173
y: 0.184
z: 0.204
}
type: TYPE_BOX
texture: "druid/pixel"
id: "line_second_2"
pivot: PIVOT_N
parent: "content"
inherit_alpha: true
}
material: "/builtins/materials/gui.material"
adjust_reference: ADJUST_REFERENCE_PARENT

View File

@@ -0,0 +1,101 @@
local helper = require("druid.helper")
local mini_graph = require("druid.widget.mini_graph.mini_graph")
---@class widget.fps_panel: druid.widget
---@field root node
local M = {}
local TARGET_FPS = sys.get_config_int("display.update_frequency", 60)
if TARGET_FPS == 0 then
TARGET_FPS = 60
end
function M:init()
self.root = self:get_node("root")
self.delta_time = 0.1 -- in seconds
self.collect_time = 3 -- in seconds
self.collect_time_counter = 0
self.graph_samples = self.collect_time / self.delta_time
-- Store frame time in seconds last collect_time seconds
self.fps_samples = {}
self.mini_graph = self.druid:new_widget(mini_graph, "mini_graph")
self.mini_graph:set_samples(self.graph_samples) -- show last 30 seconds
self.mini_graph:set_max_value(TARGET_FPS)
do -- Set parent manually
local parent_node = self.mini_graph.content
local position = helper.get_full_position(parent_node, self.mini_graph.root)
local content = self:get_node("content")
gui.set_parent(content, self.mini_graph.content)
gui.set_position(content, -position)
end
self.text_min_fps = self.druid:new_text("text_min_fps")
self.text_fps = self.druid:new_text("text_fps")
self.timer_id = timer.delay(self.delta_time, true, function()
self:push_fps_value()
end)
self.container = self.druid:new_container(self.root)
self.container:add_container(self.mini_graph.container)
end
function M:on_remove()
timer.cancel(self.timer_id)
end
function M:update(dt)
if not self.previous_time then
self.previous_time = socket.gettime()
return
end
local current_time = socket.gettime()
local delta_time = current_time - self.previous_time
self.previous_time = current_time
self.collect_time_counter = self.collect_time_counter + delta_time
table.insert(self.fps_samples, 1, delta_time)
while self.collect_time_counter > self.collect_time do
-- Remove last
local removed_value = table.remove(self.fps_samples)
self.collect_time_counter = self.collect_time_counter - removed_value
end
end
function M:push_fps_value()
if #self.fps_samples == 0 then
return
end
local max_frame_time = 0
local average_frame_time = 0
local average_samples_count = self.delta_time
local average_collected = 0
for index = 1, #self.fps_samples do
if average_frame_time < average_samples_count then
average_frame_time = average_frame_time + self.fps_samples[index]
average_collected = average_collected + 1
end
max_frame_time = math.max(max_frame_time, self.fps_samples[index])
end
average_frame_time = average_frame_time / average_collected
self.mini_graph:push_line_value(1 / average_frame_time)
self.text_fps:set_text(tostring(math.ceil(1 / average_frame_time) .. " FPS"))
local lowest_value = math.ceil(self.mini_graph:get_lowest_value())
self.text_min_fps:set_text(lowest_value .. " lowest")
end
return M

View File

@@ -0,0 +1,242 @@
fonts {
name: "druid_text_regular"
font: "/druid/fonts/druid_text_regular.font"
}
fonts {
name: "druid_text_bold"
font: "/druid/fonts/druid_text_bold.font"
}
textures {
name: "druid"
texture: "/druid/druid.atlas"
}
nodes {
size {
x: 200.0
y: 140.0
}
type: TYPE_BOX
id: "root"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
visible: false
}
nodes {
type: TYPE_TEMPLATE
id: "mini_graph"
parent: "root"
inherit_alpha: true
template: "/druid/widget/mini_graph/mini_graph.gui"
}
nodes {
type: TYPE_BOX
id: "mini_graph/root"
parent: "mini_graph"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "mini_graph/header"
parent: "mini_graph/root"
template_node_child: true
}
nodes {
type: TYPE_TEXT
text: "Memory"
id: "mini_graph/text_header"
parent: "mini_graph/header"
overridden_fields: 8
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "mini_graph/icon_drag"
parent: "mini_graph/header"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "mini_graph/content"
parent: "mini_graph/root"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "mini_graph/prefab_line"
parent: "mini_graph/content"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "mini_graph/color_low"
parent: "mini_graph/content"
template_node_child: true
}
nodes {
size {
x: 200.0
y: 100.0
}
type: TYPE_BOX
id: "content"
parent: "root"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
visible: false
}
nodes {
position {
x: -96.0
y: 12.0
}
scale {
x: 0.3
y: 0.3
}
size {
x: 200.0
y: 40.0
}
color {
x: 0.463
y: 0.475
z: 0.49
}
type: TYPE_TEXT
text: "120.23 KB"
font: "druid_text_regular"
id: "text_max_value"
pivot: PIVOT_W
outline {
x: 1.0
y: 1.0
z: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
}
parent: "content"
inherit_alpha: true
outline_alpha: 0.0
shadow_alpha: 0.0
}
nodes {
position {
x: 96.0
y: 12.0
}
scale {
x: 0.3
y: 0.3
}
size {
x: 200.0
y: 40.0
}
color {
x: 0.463
y: 0.475
z: 0.49
}
type: TYPE_TEXT
text: "120 KB/s"
font: "druid_text_regular"
id: "text_per_second"
pivot: PIVOT_E
outline {
x: 1.0
y: 1.0
z: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
}
parent: "content"
inherit_alpha: true
outline_alpha: 0.0
shadow_alpha: 0.0
}
nodes {
position {
x: -33.4
y: 30.0
}
size {
x: 3.0
y: 8.0
}
color {
x: 0.173
y: 0.184
z: 0.204
}
type: TYPE_BOX
texture: "druid/pixel"
id: "line_second_1"
pivot: PIVOT_N
parent: "content"
inherit_alpha: true
}
nodes {
position {
x: 33.2
y: 30.0
}
size {
x: 3.0
y: 8.0
}
color {
x: 0.173
y: 0.184
z: 0.204
}
type: TYPE_BOX
texture: "druid/pixel"
id: "line_second_2"
pivot: PIVOT_N
parent: "content"
inherit_alpha: true
}
nodes {
position {
y: 12.0
}
scale {
x: 0.3
y: 0.3
}
size {
x: 200.0
y: 40.0
}
color {
x: 0.463
y: 0.475
z: 0.49
}
type: TYPE_TEXT
text: "120 KB"
font: "druid_text_bold"
id: "text_memory"
outline {
x: 1.0
y: 1.0
z: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
}
parent: "content"
inherit_alpha: true
outline_alpha: 0.0
shadow_alpha: 0.0
}
material: "/builtins/materials/gui.material"
adjust_reference: ADJUST_REFERENCE_PARENT

View File

@@ -0,0 +1,96 @@
local helper = require("druid.helper")
local mini_graph = require("druid.widget.mini_graph.mini_graph")
---@class widget.memory_panel: druid.widget
---@field root node
local M = {}
function M:init()
self.root = self:get_node("root")
self.delta_time = 0.1
self.samples_count = 30
self.memory_limit = 100
self.mini_graph = self.druid:new_widget(mini_graph, "mini_graph")
self.mini_graph:set_samples(self.samples_count)
-- This one is not works with scaled root
--gui.set_parent(self:get_node("content"), self.mini_graph.content, true)
do -- Set parent manually
local parent_node = self.mini_graph.content
local position = helper.get_full_position(parent_node, self.mini_graph.root)
local content = self:get_node("content")
gui.set_parent(content, self.mini_graph.content)
gui.set_position(content, -position)
end
self.max_value = self.druid:new_text("text_max_value")
self.text_per_second = self.druid:new_text("text_per_second")
self.text_memory = self.druid:new_text("text_memory")
self.memory = collectgarbage("count")
self.memory_samples = {}
self:update_text_memory()
self.timer_id = timer.delay(self.delta_time, true, function()
self:push_next_value()
end)
self.container = self.druid:new_container(self.root)
self.container:add_container(self.mini_graph.container)
end
function M:on_remove()
timer.cancel(self.timer_id)
end
function M:set_low_memory_limit(limit)
self.memory_limit = limit
end
function M:push_next_value()
local memory = collectgarbage("count")
local diff = math.max(0, memory - self.memory)
self.memory = memory
self:update_text_memory()
table.insert(self.memory_samples, diff)
if #self.memory_samples > self.samples_count then
table.remove(self.memory_samples, 1)
end
self.mini_graph:push_line_value(diff)
local max_value = math.max(unpack(self.memory_samples))
max_value = math.max(max_value, self.memory_limit) -- low limit to display
self.mini_graph:set_max_value(max_value)
local max_memory = math.ceil(self.mini_graph:get_highest_value())
self.max_value:set_text(max_memory .. " KB")
local last_second = 0
local last_second_samples = math.ceil(1 / self.delta_time)
for index = #self.memory_samples - last_second_samples + 1, #self.memory_samples do
last_second = last_second + (self.memory_samples[index] or 0)
end
self.text_per_second:set_text(math.ceil(last_second) .. " KB/s")
end
function M:update_text_memory()
local memory = math.ceil(collectgarbage("count")) -- in KB
if memory > 1024 then
memory = memory / 1024
self.text_memory:set_text(string.format("%.2f", memory) .. " MB")
else
self.text_memory:set_text(memory .. " KB")
end
end
return M

View File

@@ -0,0 +1,176 @@
fonts {
name: "druid_text_regular"
font: "/druid/fonts/druid_text_regular.font"
}
fonts {
name: "druid_text_bold"
font: "/druid/fonts/druid_text_bold.font"
}
textures {
name: "druid"
texture: "/druid/druid.atlas"
}
nodes {
size {
x: 200.0
y: 140.0
}
color {
x: 0.173
y: 0.184
z: 0.204
}
type: TYPE_BOX
texture: "druid/ui_circle_16"
id: "root"
inherit_alpha: true
slice9 {
x: 8.0
y: 8.0
z: 8.0
w: 8.0
}
}
nodes {
position {
y: 70.0
}
size {
x: 200.0
y: 40.0
}
type: TYPE_BOX
id: "header"
pivot: PIVOT_N
parent: "root"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
visible: false
}
nodes {
position {
x: -92.0
y: -8.0
}
scale {
x: 0.5
y: 0.5
}
size {
x: 260.0
y: 50.0
}
color {
x: 0.463
y: 0.475
z: 0.49
}
type: TYPE_TEXT
text: "Mini Graph"
font: "druid_text_bold"
id: "text_header"
pivot: PIVOT_NW
outline {
x: 1.0
y: 1.0
z: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
}
parent: "header"
inherit_alpha: true
outline_alpha: 0.0
shadow_alpha: 0.0
}
nodes {
position {
x: 96.0
y: -4.0
}
color {
x: 0.306
y: 0.31
z: 0.314
}
type: TYPE_BOX
texture: "druid/icon_drag"
id: "icon_drag"
pivot: PIVOT_NE
parent: "header"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
}
nodes {
position {
y: -70.0
}
size {
x: 200.0
y: 100.0
}
color {
x: 0.129
y: 0.141
z: 0.157
}
type: TYPE_BOX
texture: "druid/ui_circle_16"
id: "content"
pivot: PIVOT_S
parent: "root"
inherit_alpha: true
slice9 {
x: 8.0
y: 8.0
z: 8.0
w: 8.0
}
clipping_mode: CLIPPING_MODE_STENCIL
material: "gui_stencil"
}
nodes {
size {
x: 8.0
y: 70.0
}
color {
x: 0.957
y: 0.608
z: 0.608
}
type: TYPE_BOX
texture: "druid/pixel"
id: "prefab_line"
pivot: PIVOT_S
parent: "content"
inherit_alpha: true
}
nodes {
position {
x: -10.0
y: 4.0
}
size {
x: 8.0
y: 8.0
}
color {
x: 0.557
y: 0.835
z: 0.62
}
type: TYPE_BOX
texture: "druid/pixel"
id: "color_low"
parent: "content"
inherit_alpha: true
}
material: "/builtins/materials/gui.material"
adjust_reference: ADJUST_REFERENCE_PARENT
materials {
name: "gui_stencil"
material: "/druid/materials/stencil/gui_stencil.material"
}

View File

@@ -0,0 +1,162 @@
local color = require("druid.color")
local helper = require("druid.helper")
---@class widget.mini_graph: druid.widget
local M = {}
local SIZE_Y = hash("size.y")
function M:init()
self.root = self:get_node("root")
self.text_header = self.druid:new_text("text_header")
self.druid:new_drag("header", self.on_drag_widget)
self.druid:new_button("icon_drag", self.toggle_hide)
:set_style(nil)
self.content = self:get_node("content")
self.layout = self.druid:new_layout(self.content, "horizontal")
:set_margin(0, 0)
:set_padding(0, 0, 0, 0)
self.prefab_line = self:get_node("prefab_line")
gui.set_enabled(self.prefab_line, false)
local node_color_low = self:get_node("color_low")
self.color_zero = gui.get_color(node_color_low)
self.color_one = gui.get_color(self.prefab_line)
gui.set_enabled(node_color_low, false)
self.is_hidden = false
self.max_value = 1 -- in this value line will be at max height
self.lines = {}
self.values = {}
self.container = self.druid:new_container(self.root)
self.container:add_container("header")
self.default_size = self.container:get_size()
end
function M:on_remove()
self:clear()
end
function M:clear()
self.layout:clear_layout()
for index = 1, #self.lines do
gui.delete_node(self.lines[index])
end
self.lines = {}
end
function M:set_samples(samples)
self.samples = samples
self:clear()
local line_width = self.layout:get_size().x / self.samples
for index = 1, self.samples do
local line = gui.clone(self.prefab_line)
gui.set_enabled(line, true)
gui.set(line, "size.x", line_width)
self.layout:add(line)
table.insert(self.lines, line)
end
end
function M:get_samples()
return self.samples
end
---Set normalized to control the color of the line
--- for index = 1, mini_graph:get_samples() do
--- mini_graph:set_line_value(index, math.random())
--- end
---@param index number
---@param value number The normalized value from 0 to 1
function M:set_line_value(index, value)
local line = self.lines[index]
if not line then
return
end
self.values[index] = value
local normalized = helper.clamp(value/self.max_value, 0, 1)
local target_color = color.lerp(normalized, self.color_zero, self.color_one)
gui.set_color(line, target_color)
self:set_line_height(index)
end
---@return number
function M:get_line_value(index)
return self.values[index] or 0
end
function M:push_line_value(value)
for index = 1, self.samples - 1 do
self:set_line_value(index, self:get_line_value(index + 1))
end
self:set_line_value(self.samples, value)
end
function M:set_max_value(max_value)
if self.max_value == max_value then
return
end
self.max_value = max_value
for index = 1, self.samples do
self:set_line_height(index)
end
end
function M:set_line_height(index)
local value = self.values[index] or 0
local normalized = helper.clamp(value / self.max_value, 0, 1)
local size_y = normalized * 70
gui.set(self.lines[index], SIZE_Y, size_y)
end
function M:get_lowest_value()
return math.min(unpack(self.values))
end
function M:get_highest_value()
return math.max(unpack(self.values))
end
function M:on_drag_widget(dx, dy)
local position = self.container:get_position()
self.container:set_position(position.x + dx, position.y + dy)
end
function M:toggle_hide()
self.is_hidden = not self.is_hidden
local hidden_size = gui.get_size(self:get_node("header"))
local new_size = self.is_hidden and hidden_size or self.default_size
self.container:set_size(new_size.x, new_size.y, gui.PIVOT_N)
gui.set_enabled(self.content, not self.is_hidden)
return self
end
return M

View File

@@ -0,0 +1,195 @@
local helper = require("druid.helper")
local event_queue = require("druid.event_queue")
---@class druid.node_repeat: druid.widget
---@field animation table
---@field node node
---@field params vector4
---@field time number
local M = {}
function M:init(node)
self.node = self:get_node(node)
self.animation = nil
gui.set_material(self.node, hash("gui_repeat"))
self.time = 0
self.margin = 0
self.params = gui.get(self.node, "params") --[[@as vector4]]
self:get_atlas_path(function(atlas_path)
self.is_inited = self:init_tiling_animation(atlas_path)
local repeat_x, repeat_y = self:get_repeat()
self:animate(repeat_x, repeat_y)
end)
--self.druid.events.on_node_property_changed:subscribe(self.on_node_property_changed, self)
end
function M:on_node_property_changed(node, property)
if not self.is_inited or node ~= self.node then
return
end
if property == "size" or property == "scale" then
local repeat_x, repeat_y = self:get_repeat()
self:set_repeat(repeat_x, repeat_y)
end
end
function M:get_repeat()
if not self.is_inited then
return 1, 1
end
local size_x = gui.get(self.node, helper.PROP_SIZE_X)
local size_y = gui.get(self.node, helper.PROP_SIZE_Y)
local scale_x = gui.get(self.node, helper.PROP_SCALE_X)
local scale_y = gui.get(self.node, helper.PROP_SCALE_Y)
local repeat_x = (size_x / self.animation.width) / scale_x
local repeat_y = (size_y / self.animation.height) / scale_y
return repeat_x, repeat_y
end
function M:get_atlas_path(callback)
event_queue.request("druid.get_atlas_path", callback, gui.get_texture(self.node), msg.url())
end
---@return boolean
function M:init_tiling_animation(atlas_path)
if not atlas_path then
print("No atlas path found for node", gui.get_id(self.node), gui.get_texture(self.node))
print("Probably you should add druid.script at window collection to access resources")
return false
end
self.animation = helper.get_animation_data_from_node(self.node, atlas_path)
return true
end
-- Start our repeat shader work
-- @param repeat_x -- X factor
-- @param repeat_y -- Y factor
function M:animate(repeat_x, repeat_y)
if not self.is_inited then
return
end
local node = self.node
local animation = self.animation
local frame = animation.frames[1]
gui.set(node, "uv_coord", frame.uv_coord)
self:set_repeat(repeat_x, repeat_y)
if #animation.frames > 1 and animation.fps > 0 then
animation.handle =
timer.delay(1/animation.fps, true, function(self, handle, time_elapsed)
local next_rame = animation.frames[animation.current_frame]
gui.set(node, "uv_coord", next_rame.uv_coord)
animation.current_frame = animation.current_frame + 1
if animation.current_frame > #animation.frames then
animation.current_frame = 1
end
end)
end
end
function M:final()
local animation = self.animation
if animation.handle then
timer.cancel(animation.handle)
animation.handle = nil
end
end
-- Update repeat factor values
-- @param repeat_x
-- @param repeat_y
function M:set_repeat(repeat_x, repeat_y)
local animation = self.animation
animation.v.x = repeat_x or animation.v.x
animation.v.y = repeat_y or animation.v.y
local anchor = helper.get_pivot_offset(gui.get_pivot(self.node))
animation.v.z = anchor.x
animation.v.w = anchor.y
gui.set(self.node, "uv_repeat", animation.v)
end
function M:set_perpective(perspective_x, perspective_y)
if perspective_x then
gui.set(self.node, "perspective.x", perspective_x)
end
if perspective_y then
gui.set(self.node, "perspective.y", perspective_y)
end
return self
end
function M:set_perpective_offset(offset_x, offset_y)
if offset_x then
gui.set(self.node, "perspective.z", offset_x)
end
if offset_y then
gui.set(self.node, "perspective.w", offset_y)
end
return self
end
function M:set_offset(offset_perc_x, offset_perc_y)
self.params.z = offset_perc_x or self.params.z
self.params.w = offset_perc_y or self.params.w
gui.set(self.node, "params", self.params)
return self
end
function M:set_margin(margin_x, margin_y)
self.params.x = margin_x or self.params.x
self.params.y = margin_y or self.params.y
gui.set(self.node, "params", self.params)
return self
end
---@param scale number
function M:set_scale(scale)
local current_scale_x = gui.get(self.node, helper.PROP_SCALE_X)
local current_scale_y = gui.get(self.node, helper.PROP_SCALE_Y)
local current_size_x = gui.get(self.node, helper.PROP_SIZE_X)
local current_size_y = gui.get(self.node, helper.PROP_SIZE_Y)
local delta_scale_x = scale / current_scale_x
local delta_scale_y = scale / current_scale_y
gui.set(self.node, helper.PROP_SCALE_X, scale)
gui.set(self.node, helper.PROP_SCALE_Y, scale)
gui.set(self.node, helper.PROP_SIZE_X, current_size_x / delta_scale_x)
gui.set(self.node, helper.PROP_SIZE_Y, current_size_y / delta_scale_y)
--self.druid:on_node_property_changed(self.node, "scale")
--self.druid:on_node_property_changed(self.node, "size")
--local repeat_x, repeat_y = self:get_repeat()
--self:set_repeat(repeat_x, repeat_y)
return self
end
return M

View File

@@ -0,0 +1,155 @@
fonts {
name: "druid_text_bold"
font: "/druid/fonts/druid_text_bold.font"
}
textures {
name: "druid"
texture: "/druid/druid.atlas"
}
nodes {
size {
x: 400.0
y: 40.0
}
type: TYPE_BOX
texture: "druid/empty"
id: "root"
adjust_mode: ADJUST_MODE_STRETCH
inherit_alpha: true
visible: false
}
nodes {
position {
x: -200.0
}
scale {
x: 0.5
y: 0.5
}
size {
x: 350.0
y: 40.0
}
color {
x: 0.463
y: 0.475
z: 0.49
}
type: TYPE_TEXT
text: "Button"
font: "druid_text_bold"
id: "text_name"
pivot: PIVOT_W
outline {
x: 1.0
y: 1.0
z: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
}
adjust_mode: ADJUST_MODE_STRETCH
parent: "root"
inherit_alpha: true
outline_alpha: 0.0
shadow_alpha: 0.0
}
nodes {
position {
x: 200.0
}
size {
x: 200.0
y: 40.0
}
type: TYPE_BOX
id: "E_Anchor"
pivot: PIVOT_E
parent: "root"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
visible: false
}
nodes {
position {
x: -100.0
}
size {
x: 200.0
y: 40.0
}
color {
x: 0.463
y: 0.475
z: 0.49
}
type: TYPE_BOX
texture: "druid/rect_round2_width2"
id: "button"
parent: "E_Anchor"
inherit_alpha: true
slice9 {
x: 5.0
y: 5.0
z: 5.0
w: 5.0
}
}
nodes {
position {
y: -20.0
}
size {
x: 200.0
y: 4.0
}
color {
x: 0.894
y: 0.506
z: 0.333
}
type: TYPE_BOX
texture: "druid/pixel"
id: "selected"
pivot: PIVOT_S
adjust_mode: ADJUST_MODE_STRETCH
parent: "button"
inherit_alpha: true
}
nodes {
scale {
x: 0.5
y: 0.5
}
size {
x: 380.0
y: 50.0
}
color {
x: 0.722
y: 0.741
z: 0.761
}
type: TYPE_TEXT
text: "Button"
font: "druid_text_bold"
id: "text_button"
outline {
x: 1.0
y: 1.0
z: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
}
parent: "button"
inherit_alpha: true
outline_alpha: 0.0
shadow_alpha: 0.0
}
material: "/builtins/materials/gui.material"
adjust_reference: ADJUST_REFERENCE_PARENT

View File

@@ -0,0 +1,58 @@
local color = require("druid.color")
---@class widget.property_button: druid.widget
---@field root node
---@field container druid.container
---@field text_name druid.text
---@field button druid.button
---@field text_button druid.text
---@field druid druid_instance
local M = {}
function M:init()
self.root = self:get_node("root")
self.text_name = self.druid:new_text("text_name")
:set_text_adjust("scale_then_trim", 0.3)
self.selected = self:get_node("selected")
gui.set_alpha(self.selected, 0)
self.button = self.druid:new_button("button", self.on_click)
self.text_button = self.druid:new_text("text_button")
self.container = self.druid:new_container(self.root)
self.container:add_container("text_name", nil, function(_, size)
self.text_button:set_size(size)
end)
self.container:add_container("E_Anchor")
end
function M:on_click()
gui.set_alpha(self.selected, 1)
gui.animate(self.selected, "color.w", 0, gui.EASING_INSINE, 0.16)
end
---@param text string
---@return widget.property_button
function M:set_text_property(text)
self.text_name:set_text(text)
return self
end
---@param text string
---@return widget.property_button
function M:set_text_button(text)
self.text_button:set_text(text)
return self
end
function M:set_color(color_value)
color.set_color(self:get_node("button"), color_value)
end
return M

View File

@@ -0,0 +1,134 @@
fonts {
name: "druid_text_bold"
font: "/druid/fonts/druid_text_bold.font"
}
textures {
name: "druid"
texture: "/druid/druid.atlas"
}
nodes {
size {
x: 400.0
y: 40.0
}
type: TYPE_BOX
texture: "druid/empty"
id: "root"
adjust_mode: ADJUST_MODE_STRETCH
inherit_alpha: true
visible: false
}
nodes {
position {
x: -200.0
}
scale {
x: 0.5
y: 0.5
}
size {
x: 360.0
y: 40.0
}
color {
x: 0.463
y: 0.475
z: 0.49
}
type: TYPE_TEXT
text: "Checkbox"
font: "druid_text_bold"
id: "text_name"
pivot: PIVOT_W
outline {
x: 1.0
y: 1.0
z: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
}
parent: "root"
inherit_alpha: true
outline_alpha: 0.0
shadow_alpha: 0.0
}
nodes {
position {
x: 200.0
}
size {
x: 200.0
y: 40.0
}
type: TYPE_BOX
id: "E_Anchor"
pivot: PIVOT_E
parent: "root"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
visible: false
}
nodes {
position {
x: -180.0
}
size {
x: 40.0
y: 40.0
}
color {
x: 0.463
y: 0.475
z: 0.49
}
type: TYPE_BOX
texture: "druid/rect_round2_width2"
id: "button"
parent: "E_Anchor"
inherit_alpha: true
slice9 {
x: 5.0
y: 5.0
z: 5.0
w: 5.0
}
}
nodes {
color {
x: 0.722
y: 0.741
z: 0.761
}
type: TYPE_BOX
texture: "druid/ui_circle_16"
id: "icon"
parent: "button"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
}
nodes {
position {
y: -20.0
}
size {
x: 40.0
y: 4.0
}
color {
x: 0.894
y: 0.506
z: 0.333
}
type: TYPE_BOX
texture: "druid/pixel"
id: "selected"
pivot: PIVOT_S
adjust_mode: ADJUST_MODE_STRETCH
parent: "button"
inherit_alpha: true
}
material: "/builtins/materials/gui.material"
adjust_reference: ADJUST_REFERENCE_PARENT

View File

@@ -0,0 +1,76 @@
local event = require("event.event")
---@class widget.property_checkbox: druid.widget
---@field root node
---@field druid druid_instance
---@field text_name druid.text
---@field button druid.button
---@field selected node
local M = {}
function M:init()
self.root = self:get_node("root")
self.icon = self:get_node("icon")
gui.set_enabled(self.icon, false)
self.selected = self:get_node("selected")
gui.set_alpha(self.selected, 0)
self.text_name = self.druid:new_text("text_name")
:set_text_adjust("scale_then_trim", 0.3)
self.button = self.druid:new_button("button", self.on_click)
self.container = self.druid:new_container(self.root)
self.container:add_container("text_name")
self.container:add_container("E_Anchor")
self.on_change_value = event.create()
end
---@param value boolean
function M:set_value(value, is_instant)
if self._value == value then
return
end
self._value = value
gui.set_enabled(self.icon, value)
self.on_change_value:trigger(value)
if not is_instant then
gui.set_alpha(self.selected, 1)
gui.animate(self.selected, "color.w", 0, gui.EASING_INSINE, 0.16)
end
end
---@return boolean
function M:get_value()
return self._value
end
function M:on_click()
self:set_value(not self:get_value())
end
--- Set the text property of the checkbox
---@param text string
function M:set_text_property(text)
self.text_name:set_text(text)
end
--- Set the callback function for when the checkbox value changes
---@param callback function
function M:on_change(callback)
self.on_change_value:subscribe(callback)
end
return M

View File

@@ -0,0 +1,143 @@
fonts {
name: "druid_text_bold"
font: "/druid/fonts/druid_text_bold.font"
}
textures {
name: "druid"
texture: "/druid/druid.atlas"
}
nodes {
size {
x: 400.0
y: 40.0
}
type: TYPE_BOX
texture: "druid/empty"
id: "root"
adjust_mode: ADJUST_MODE_STRETCH
inherit_alpha: true
visible: false
}
nodes {
position {
x: -200.0
}
scale {
x: 0.5
y: 0.5
}
size {
x: 350.0
y: 50.0
}
color {
x: 0.463
y: 0.475
z: 0.49
}
type: TYPE_TEXT
text: "Input"
font: "druid_text_bold"
id: "text_name"
pivot: PIVOT_W
outline {
x: 1.0
y: 1.0
z: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
}
parent: "root"
inherit_alpha: true
outline_alpha: 0.0
shadow_alpha: 0.0
}
nodes {
position {
x: 200.0
}
size {
x: 200.0
y: 40.0
}
type: TYPE_BOX
id: "E_Anchor"
pivot: PIVOT_E
parent: "root"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
visible: false
}
nodes {
position {
x: -100.0
}
type: TYPE_TEMPLATE
id: "rich_input"
parent: "E_Anchor"
inherit_alpha: true
template: "/druid/custom/rich_input/rich_input.gui"
}
nodes {
type: TYPE_BOX
id: "rich_input/root"
parent: "rich_input"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "rich_input/button"
parent: "rich_input/root"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "rich_input/placeholder_text"
parent: "rich_input/root"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "rich_input/input_text"
parent: "rich_input/root"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "rich_input/cursor_node"
parent: "rich_input/root"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "rich_input/cursor_text"
parent: "rich_input/cursor_node"
template_node_child: true
}
nodes {
position {
x: -100.0
y: -20.0
}
size {
x: 200.0
y: 4.0
}
color {
x: 0.894
y: 0.506
z: 0.333
}
type: TYPE_BOX
texture: "druid/pixel"
id: "selected"
pivot: PIVOT_S
adjust_mode: ADJUST_MODE_STRETCH
parent: "E_Anchor"
inherit_alpha: true
}
material: "/builtins/materials/gui.material"
adjust_reference: ADJUST_REFERENCE_PARENT

View File

@@ -0,0 +1,46 @@
---@class widget.property_input: druid.widget
---@field root node
---@field container druid.container
---@field text_name druid.text
---@field button druid.button
---@field druid druid_instance
local M = {}
function M:init()
self.root = self:get_node("root")
self.text_name = self.druid:new_text("text_name")
:set_text_adjust("scale_then_trim", 0.3)
self.selected = self:get_node("selected")
gui.set_alpha(self.selected, 0)
self.rich_input = self.druid:new_rich_input("rich_input")
self.container = self.druid:new_container(self.root)
self.container:add_container("text_name")
self.container:add_container("E_Anchor")
end
---@param text string
---@return widget.property_input
function M:set_text_property(text)
self.text_name:set_text(text)
return self
end
---@param text string
---@return widget.property_input
function M:set_text_value(text)
self.rich_input:set_text(text)
return self
end
function M:on_change(callback, callback_context)
self.rich_input.input.on_input_unselect:subscribe(callback, callback_context)
end
return M

View File

@@ -0,0 +1,212 @@
fonts {
name: "druid_text_bold"
font: "/druid/fonts/druid_text_bold.font"
}
textures {
name: "druid"
texture: "/druid/druid.atlas"
}
nodes {
size {
x: 400.0
y: 40.0
}
type: TYPE_BOX
texture: "druid/empty"
id: "root"
adjust_mode: ADJUST_MODE_STRETCH
inherit_alpha: true
visible: false
}
nodes {
position {
x: -200.0
}
scale {
x: 0.5
y: 0.5
}
size {
x: 360.0
y: 40.0
}
color {
x: 0.463
y: 0.475
z: 0.49
}
type: TYPE_TEXT
text: "Left Right Selector"
font: "druid_text_bold"
id: "text_name"
pivot: PIVOT_W
outline {
x: 1.0
y: 1.0
z: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
}
parent: "root"
inherit_alpha: true
outline_alpha: 0.0
shadow_alpha: 0.0
}
nodes {
position {
x: 200.0
}
size {
x: 200.0
y: 40.0
}
type: TYPE_BOX
id: "E_Anchor"
pivot: PIVOT_E
parent: "root"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
visible: false
}
nodes {
position {
x: -180.0
}
size {
x: 40.0
y: 40.0
}
color {
x: 0.463
y: 0.475
z: 0.49
}
type: TYPE_BOX
texture: "druid/rect_round2_width2"
id: "button_left"
parent: "E_Anchor"
inherit_alpha: true
slice9 {
x: 5.0
y: 5.0
z: 5.0
w: 5.0
}
}
nodes {
rotation {
z: 180.0
}
color {
x: 0.722
y: 0.741
z: 0.761
}
type: TYPE_BOX
texture: "druid/icon_arrow"
id: "icon_left"
parent: "button_left"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
}
nodes {
position {
x: -20.0
}
size {
x: 40.0
y: 40.0
}
color {
x: 0.463
y: 0.475
z: 0.49
}
type: TYPE_BOX
texture: "druid/rect_round2_width2"
id: "button_right"
parent: "E_Anchor"
inherit_alpha: true
slice9 {
x: 5.0
y: 5.0
z: 5.0
w: 5.0
}
}
nodes {
color {
x: 0.722
y: 0.741
z: 0.761
}
type: TYPE_BOX
texture: "druid/icon_arrow"
id: "icon_right"
parent: "button_right"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
}
nodes {
position {
x: -100.0
y: -20.0
}
size {
x: 120.0
y: 4.0
}
color {
x: 0.894
y: 0.506
z: 0.333
}
type: TYPE_BOX
texture: "druid/pixel"
id: "selected"
pivot: PIVOT_S
adjust_mode: ADJUST_MODE_STRETCH
parent: "E_Anchor"
inherit_alpha: true
}
nodes {
position {
x: -100.0
}
scale {
x: 0.5
y: 0.5
}
size {
x: 220.0
y: 40.0
}
color {
x: 0.463
y: 0.475
z: 0.49
}
type: TYPE_TEXT
text: "42"
font: "druid_text_bold"
id: "text_value"
outline {
x: 1.0
y: 1.0
z: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
}
parent: "E_Anchor"
inherit_alpha: true
outline_alpha: 0.0
shadow_alpha: 0.0
}
material: "/builtins/materials/gui.material"
adjust_reference: ADJUST_REFERENCE_PARENT

View File

@@ -0,0 +1,211 @@
local event = require("event.event")
---@class widget.property_left_right_selector: druid.widget
---@field root node
---@field druid druid_instance
---@field text_name druid.text
---@field button druid.button
---@field selected node
---@field value string|number
---@field on_change_value event fun(value: string|number)
local M = {}
function M:init()
self.root = self:get_node("root")
self.selected = self:get_node("selected")
gui.set_alpha(self.selected, 0)
self.text_name = self.druid:new_text("text_name")
:set_text_adjust("scale_then_trim", 0.3)
self.text_value = self.druid:new_text("text_value")
self.button_left = self.druid:new_button("button_left", self.on_button_left)
self.button_left.on_repeated_click:subscribe(self.on_button_left, self)
self.button_right = self.druid:new_button("button_right", self.on_button_right)
self.button_right.on_repeated_click:subscribe(self.on_button_right, self)
self.on_change_value = event.create()
self.container = self.druid:new_container(self.root)
self.container:add_container("text_name")
self.container:add_container("E_Anchor")
end
function M:set_text(text)
self.text_name:set_text(text)
return self
end
---Helper to cycle number in range
---@param value number Current value
---@param min number Min range value
---@param max number Max range value
---@param step number Step size
---@param is_loop boolean Is looped
---@return number Cycled value
local function step_number(value, min, max, step, is_loop)
local range = max - min + 1
if is_loop then
-- Normalize step within range
local effective_step = step
if math.abs(step) >= range then
effective_step = step % range
if effective_step == 0 then
effective_step = step > 0 and range or -range
end
end
value = value + effective_step
-- Handle wrapping
if max then
while value > max do
value = min + (value - max - 1)
end
end
if min then
while value < min do
value = max - (min - value - 1)
end
end
else
-- Clamp values
value = value + step
if max and value > max then
return max
elseif min and value < min then
return min
end
end
return value
end
---Helper to cycle array index with proper step wrapping
---@param array table Array to cycle through
---@param current_value any Current value to find index for
---@param step number Step direction
---@param is_loop boolean If true, cycle values. If false, clamp at ends
---@return any Next value in cycle
local function step_array(array, current_value, step, is_loop)
local index = 1
for i, v in ipairs(array) do
if v == current_value then
index = i
break
end
end
if is_loop then
-- Normalize step within array length
local range = #array
local effective_step = step
if math.abs(step) >= range then
effective_step = step % range
if effective_step == 0 then
effective_step = step > 0 and range or -range
end
end
index = index + effective_step
-- Handle wrapping
while index > range do
index = 1 + (index - range - 1)
end
while index < 1 do
index = range - (1 - index - 1)
end
else
-- Clamp values
index = index + step
if index > #array then
index = #array
elseif index < 1 then
index = 1
end
end
return array[index]
end
function M:on_button_left()
self:add_step(-1)
end
function M:on_button_right()
self:add_step(1)
end
---@param koef number -1 0 1, on 0 will not move
function M:add_step(koef)
local array_type = self.array_type
if array_type then
local value = self.value
local new_value = step_array(array_type.array, value, koef * array_type.steps, array_type.is_loop)
self:set_value(new_value)
return
end
local number_type = self.number_type
if number_type then
local value = tonumber(self.value) --[[@as number]]
local new_value = step_number(value, number_type.min, number_type.max, koef * number_type.steps, number_type.is_loop)
self:set_value(new_value)
return
end
end
function M:set_number_type(min, max, is_loop, steps)
self.number_type = {
min = min,
max = max,
steps = steps or 1,
is_loop = is_loop,
}
return self
end
function M:set_array_type(array, is_loop, steps)
self.array_type = {
array = array,
steps = steps or 1,
is_loop = is_loop,
}
return self
end
---@param value string|number
function M:set_value(value, is_instant)
if self.value == value then
return
end
self.value = value
self.text_value:set_text(tostring(value))
self.on_change_value:trigger(value)
if not is_instant then
gui.set_alpha(self.selected, 1)
gui.animate(self.selected, "color.w", 0, gui.EASING_INSINE, 0.16)
end
end
---@return string|number
function M:get_value()
return self.value
end
return M

View File

@@ -0,0 +1,222 @@
fonts {
name: "druid_text_bold"
font: "/druid/fonts/druid_text_bold.font"
}
textures {
name: "druid"
texture: "/druid/druid.atlas"
}
nodes {
size {
x: 400.0
y: 40.0
}
type: TYPE_BOX
texture: "druid/empty"
id: "root"
adjust_mode: ADJUST_MODE_STRETCH
inherit_alpha: true
visible: false
}
nodes {
position {
x: -200.0
}
scale {
x: 0.5
y: 0.5
}
size {
x: 380.0
y: 40.0
}
color {
x: 0.463
y: 0.475
z: 0.49
}
type: TYPE_TEXT
text: "Slider"
font: "druid_text_bold"
id: "text_name"
pivot: PIVOT_W
outline {
x: 1.0
y: 1.0
z: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
}
parent: "root"
inherit_alpha: true
outline_alpha: 0.0
shadow_alpha: 0.0
}
nodes {
position {
x: 200.0
}
size {
x: 200.0
y: 40.0
}
type: TYPE_BOX
id: "E_Anchor"
pivot: PIVOT_E
adjust_mode: ADJUST_MODE_STRETCH
parent: "root"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
visible: false
}
nodes {
position {
x: -133.0
}
size {
x: 134.0
y: 40.0
}
color {
x: 0.129
y: 0.141
z: 0.157
}
type: TYPE_BOX
texture: "druid/empty"
id: "slider"
parent: "E_Anchor"
inherit_alpha: true
}
nodes {
size {
x: 134.0
y: 8.0
}
color {
x: 0.129
y: 0.141
z: 0.157
}
type: TYPE_BOX
texture: "druid/ui_circle_8"
id: "slider_back"
parent: "slider"
inherit_alpha: true
slice9 {
x: 4.0
y: 4.0
z: 4.0
w: 4.0
}
}
nodes {
position {
x: -55.0
}
size {
x: 24.0
y: 24.0
}
color {
x: 0.722
y: 0.741
z: 0.761
}
type: TYPE_BOX
texture: "druid/ui_circle_8"
id: "slider_pin"
parent: "slider"
inherit_alpha: true
slice9 {
x: 4.0
y: 4.0
z: 4.0
w: 4.0
}
}
nodes {
size {
x: 60.0
y: 40.0
}
color {
x: 0.463
y: 0.475
z: 0.49
}
type: TYPE_BOX
texture: "druid/rect_round2_width2"
id: "button"
pivot: PIVOT_E
parent: "E_Anchor"
inherit_alpha: true
slice9 {
x: 4.0
y: 4.0
z: 4.0
w: 4.0
}
}
nodes {
position {
y: -20.0
}
size {
x: 60.0
y: 4.0
}
color {
x: 0.894
y: 0.506
z: 0.333
}
type: TYPE_BOX
texture: "druid/pixel"
id: "selected"
pivot: PIVOT_SE
adjust_mode: ADJUST_MODE_STRETCH
parent: "button"
inherit_alpha: true
}
nodes {
position {
x: -30.0
}
scale {
x: 0.55
y: 0.55
}
size {
x: 100.0
y: 40.0
}
color {
x: 0.722
y: 0.741
z: 0.761
}
type: TYPE_TEXT
text: "25 %"
font: "druid_text_bold"
id: "text_value"
outline {
x: 1.0
y: 1.0
z: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
}
parent: "button"
inherit_alpha: true
outline_alpha: 0.0
shadow_alpha: 0.0
}
material: "/builtins/materials/gui.material"
adjust_reference: ADJUST_REFERENCE_PARENT

View File

@@ -0,0 +1,125 @@
local event = require("event.event")
local helper = require("druid.helper")
---@class widget.property_slider: druid.widget
---@field root node
---@field container druid.container
---@field druid druid_instance
---@field text_name druid.text
---@field text_value druid.text
---@field slider druid.slider
local M = {}
function M:init()
self.root = self:get_node("root")
self.selected = self:get_node("selected")
gui.set_alpha(self.selected, 0)
self._value = 0
self.min = 0
self.max = 1
self.step = 0.01
self.text_name = self.druid:new_text("text_name")
:set_text_adjust("scale_then_trim", 0.3)
self.text_value = self.druid:new_text("text_value")
self.slider = self.druid:new_slider("slider_pin", vmath.vector3(55, 0, 0), self.update_value) --[[@as druid.slider]]
self.slider:set_input_node("slider")
self:set_text_function(function(value)
return math.floor(value * 100) .. "%"
end)
self.container = self.druid:new_container(self.root)
self.container:add_container("text_name")
self.container:add_container("E_Anchor")
self.on_change_value = event.create()
end
---@param callback fun(value:number):string
function M:set_text_function(callback)
self._text_function = callback
self.text_value:set_text(self._text_function(self._value))
end
--- Sets the text property of the slider
---@param text string
function M:set_text_property(text)
self.text_name:set_text(text)
end
--- Sets the callback function for when the slider value changes
---@param callback fun(value:number)
function M:on_change(callback)
self.on_change_value:subscribe(callback)
end
---@param value number
function M:set_value(value, is_instant)
local diff = math.abs(self.max - self.min)
self.slider:set((value - self.min) / diff, true)
local is_changed = self._value ~= value
if not is_changed then
return
end
self._value = value
self.text_value:set_text(self._text_function(value))
self.on_change_value:trigger(value)
if not is_instant then
gui.set_alpha(self.selected, 1)
gui.animate(self.selected, "color.w", 0, gui.EASING_INSINE, 0.16)
end
end
---@return number
function M:get_value()
return self._value
end
function M:update_value(value)
local current_value = self._value
local diff = math.abs(self.max - self.min)
-- [0..1] To range
value = value * diff + self.min
-- Round to steps value (0.1, or 5. Should be divided on this value)
value = math.floor(value / self.step + 0.5) * self.step
value = helper.clamp(value, self.min, self.max)
self:set_value(value)
end
function M:set_number_type(min, max, step)
self.min = min or 0
self.max = max or 1
self.step = step
self:set_text_function(function(value)
return tostring(value)
end)
self:set_value(self._value, true)
end
function M:_on_slider_change_by_user(value)
self:set_value(value)
end
return M

View File

@@ -0,0 +1,96 @@
fonts {
name: "druid_text_bold"
font: "/druid/fonts/druid_text_bold.font"
}
textures {
name: "druid"
texture: "/druid/druid.atlas"
}
nodes {
size {
x: 400.0
y: 40.0
}
type: TYPE_BOX
texture: "druid/empty"
id: "root"
adjust_mode: ADJUST_MODE_STRETCH
inherit_alpha: true
visible: false
}
nodes {
position {
x: -200.0
}
scale {
x: 0.5
y: 0.5
}
size {
x: 400.0
y: 50.0
}
color {
x: 0.463
y: 0.475
z: 0.49
}
type: TYPE_TEXT
text: "Text"
font: "druid_text_bold"
id: "text_name"
pivot: PIVOT_W
outline {
x: 1.0
y: 1.0
z: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
}
parent: "root"
inherit_alpha: true
outline_alpha: 0.0
shadow_alpha: 0.0
}
nodes {
position {
x: 200.0
}
scale {
x: 0.5
y: 0.5
}
size {
x: 350.0
y: 50.0
}
color {
x: 0.722
y: 0.741
z: 0.761
}
type: TYPE_TEXT
text: "Text"
font: "druid_text_bold"
id: "text_right"
pivot: PIVOT_E
outline {
x: 1.0
y: 1.0
z: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
}
parent: "root"
inherit_alpha: true
outline_alpha: 0.0
shadow_alpha: 0.0
}
material: "/builtins/materials/gui.material"
adjust_reference: ADJUST_REFERENCE_PARENT

View File

@@ -0,0 +1,42 @@
---@class widget.property_text: druid.widget
---@field root node
---@field container druid.container
---@field text_name druid.text
---@field text_right druid.text
local M = {}
function M:init()
self.root = self:get_node("root")
self.text_name = self.druid:new_text("text_name")
:set_text_adjust("scale_when_trim_left", 0.3)
self.text_right = self.druid:new_text("text_right", "")
--:set_text_adjust("scale_when_trim_left", 0.3) -- TODO: not works? why?
self.container = self.druid:new_container(self.root)
self.container:add_container("text_name", nil, function(_, size)
self.text_name:set_size(size)
end)
self.container:add_container("text_right", nil, function(_, size)
self.text_right:set_size(size)
end)
end
---@param text string
---@return widget.property_text
function M:set_text_property(text)
self.text_name:set_text(text)
return self
end
---@param text string|nil
---@return widget.property_text
function M:set_text_value(text)
self.text_right:set_text(text or "")
return self
end
return M

View File

@@ -0,0 +1,490 @@
fonts {
name: "druid_text_bold"
font: "/druid/fonts/druid_text_bold.font"
}
fonts {
name: "druid_text_regular"
font: "/druid/fonts/druid_text_regular.font"
}
textures {
name: "druid"
texture: "/druid/druid.atlas"
}
nodes {
size {
x: 400.0
y: 40.0
}
type: TYPE_BOX
texture: "druid/empty"
id: "root"
adjust_mode: ADJUST_MODE_STRETCH
inherit_alpha: true
visible: false
}
nodes {
position {
x: -200.0
}
scale {
x: 0.5
y: 0.5
}
size {
x: 350.0
y: 50.0
}
color {
x: 0.463
y: 0.475
z: 0.49
}
type: TYPE_TEXT
text: "Vector3"
font: "druid_text_bold"
id: "text_name"
pivot: PIVOT_W
outline {
x: 1.0
y: 1.0
z: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
}
parent: "root"
inherit_alpha: true
outline_alpha: 0.0
shadow_alpha: 0.0
}
nodes {
position {
x: 200.0
}
size {
x: 200.0
y: 40.0
}
type: TYPE_BOX
id: "E_Anchor"
pivot: PIVOT_E
parent: "root"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
visible: false
}
nodes {
position {
x: -200.0
}
size {
x: 66.0
y: 40.0
}
type: TYPE_BOX
id: "field_x"
pivot: PIVOT_W
parent: "E_Anchor"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
visible: false
}
nodes {
position {
x: 7.0
}
scale {
x: 0.5
y: 0.5
}
size {
x: 30.0
y: 40.0
}
color {
x: 0.31
y: 0.318
z: 0.322
}
type: TYPE_TEXT
text: "X"
font: "druid_text_regular"
id: "text_x"
parent: "field_x"
inherit_alpha: true
outline_alpha: 0.0
shadow_alpha: 0.0
}
nodes {
position {
x: 40.0
}
type: TYPE_TEMPLATE
id: "rich_input_x"
parent: "field_x"
inherit_alpha: true
template: "/druid/custom/rich_input/rich_input.gui"
}
nodes {
size {
x: 50.0
y: 40.0
}
type: TYPE_BOX
id: "rich_input_x/root"
parent: "rich_input_x"
overridden_fields: 4
template_node_child: true
}
nodes {
size {
x: 50.0
y: 40.0
}
type: TYPE_BOX
id: "rich_input_x/button"
parent: "rich_input_x/root"
overridden_fields: 4
template_node_child: true
}
nodes {
size {
x: 70.0
y: 50.0
}
type: TYPE_TEXT
id: "rich_input_x/placeholder_text"
parent: "rich_input_x/root"
overridden_fields: 4
overridden_fields: 8
template_node_child: true
}
nodes {
size {
x: 70.0
y: 50.0
}
type: TYPE_TEXT
text: "20.0"
id: "rich_input_x/input_text"
parent: "rich_input_x/root"
overridden_fields: 4
overridden_fields: 8
template_node_child: true
}
nodes {
position {
x: 18.0
}
type: TYPE_BOX
id: "rich_input_x/cursor_node"
parent: "rich_input_x/root"
overridden_fields: 1
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "rich_input_x/cursor_text"
parent: "rich_input_x/cursor_node"
template_node_child: true
}
nodes {
position {
x: 40.0
y: -20.0
}
size {
x: 50.0
y: 4.0
}
color {
x: 0.894
y: 0.506
z: 0.333
}
type: TYPE_BOX
texture: "druid/pixel"
id: "selected_x"
pivot: PIVOT_S
adjust_mode: ADJUST_MODE_STRETCH
parent: "field_x"
inherit_alpha: true
}
nodes {
position {
x: -132.0
}
size {
x: 66.0
y: 40.0
}
type: TYPE_BOX
id: "field_y"
pivot: PIVOT_W
parent: "E_Anchor"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
visible: false
}
nodes {
position {
x: 7.0
}
scale {
x: 0.5
y: 0.5
}
size {
x: 30.0
y: 40.0
}
color {
x: 0.31
y: 0.318
z: 0.322
}
type: TYPE_TEXT
text: "Y"
font: "druid_text_regular"
id: "text_y"
parent: "field_y"
inherit_alpha: true
outline_alpha: 0.0
shadow_alpha: 0.0
}
nodes {
position {
x: 40.0
}
type: TYPE_TEMPLATE
id: "rich_input_y"
parent: "field_y"
inherit_alpha: true
template: "/druid/custom/rich_input/rich_input.gui"
}
nodes {
size {
x: 50.0
y: 40.0
}
type: TYPE_BOX
id: "rich_input_y/root"
parent: "rich_input_y"
overridden_fields: 4
template_node_child: true
}
nodes {
size {
x: 50.0
y: 40.0
}
type: TYPE_BOX
id: "rich_input_y/button"
parent: "rich_input_y/root"
overridden_fields: 4
template_node_child: true
}
nodes {
size {
x: 70.0
y: 50.0
}
type: TYPE_TEXT
id: "rich_input_y/placeholder_text"
parent: "rich_input_y/root"
overridden_fields: 4
overridden_fields: 8
template_node_child: true
}
nodes {
size {
x: 70.0
y: 50.0
}
type: TYPE_TEXT
text: "20.0"
id: "rich_input_y/input_text"
parent: "rich_input_y/root"
overridden_fields: 4
overridden_fields: 8
template_node_child: true
}
nodes {
position {
x: 18.0
}
type: TYPE_BOX
id: "rich_input_y/cursor_node"
parent: "rich_input_y/root"
overridden_fields: 1
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "rich_input_y/cursor_text"
parent: "rich_input_y/cursor_node"
template_node_child: true
}
nodes {
position {
x: 40.0
y: -20.0
}
size {
x: 50.0
y: 4.0
}
color {
x: 0.894
y: 0.506
z: 0.333
}
type: TYPE_BOX
texture: "druid/pixel"
id: "selected_y"
pivot: PIVOT_S
adjust_mode: ADJUST_MODE_STRETCH
parent: "field_y"
inherit_alpha: true
}
nodes {
position {
x: -66.0
}
size {
x: 66.0
y: 40.0
}
type: TYPE_BOX
id: "field_z"
pivot: PIVOT_W
parent: "E_Anchor"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
visible: false
}
nodes {
position {
x: 7.0
}
scale {
x: 0.5
y: 0.5
}
size {
x: 30.0
y: 40.0
}
color {
x: 0.31
y: 0.318
z: 0.322
}
type: TYPE_TEXT
text: "Z"
font: "druid_text_regular"
id: "text_z"
parent: "field_z"
inherit_alpha: true
outline_alpha: 0.0
shadow_alpha: 0.0
}
nodes {
position {
x: 40.0
}
type: TYPE_TEMPLATE
id: "rich_input_z"
parent: "field_z"
inherit_alpha: true
template: "/druid/custom/rich_input/rich_input.gui"
}
nodes {
size {
x: 50.0
y: 40.0
}
type: TYPE_BOX
id: "rich_input_z/root"
parent: "rich_input_z"
overridden_fields: 4
template_node_child: true
}
nodes {
size {
x: 50.0
y: 40.0
}
type: TYPE_BOX
id: "rich_input_z/button"
parent: "rich_input_z/root"
overridden_fields: 4
template_node_child: true
}
nodes {
size {
x: 70.0
y: 50.0
}
type: TYPE_TEXT
id: "rich_input_z/placeholder_text"
parent: "rich_input_z/root"
overridden_fields: 4
overridden_fields: 8
template_node_child: true
}
nodes {
size {
x: 70.0
y: 50.0
}
type: TYPE_TEXT
text: "20.0"
id: "rich_input_z/input_text"
parent: "rich_input_z/root"
overridden_fields: 4
overridden_fields: 8
template_node_child: true
}
nodes {
position {
x: 18.0
}
type: TYPE_BOX
id: "rich_input_z/cursor_node"
parent: "rich_input_z/root"
overridden_fields: 1
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "rich_input_z/cursor_text"
parent: "rich_input_z/cursor_node"
template_node_child: true
}
nodes {
position {
x: 40.0
y: -20.0
}
size {
x: 50.0
y: 4.0
}
color {
x: 0.894
y: 0.506
z: 0.333
}
type: TYPE_BOX
texture: "druid/pixel"
id: "selected_z"
pivot: PIVOT_S
adjust_mode: ADJUST_MODE_STRETCH
parent: "field_z"
inherit_alpha: true
}
material: "/builtins/materials/gui.material"
adjust_reference: ADJUST_REFERENCE_PARENT

View File

@@ -0,0 +1,75 @@
local event = require("event.event")
---@class widget.property_vector3: druid.widget
---@field root node
---@field container druid.container
---@field text_name druid.text
---@field button druid.button
---@field druid druid_instance
local M = {}
function M:init()
self.root = self:get_node("root")
self.text_name = self.druid:new_text("text_name")
:set_text_adjust("scale_then_trim", 0.3)
self.selected_x = self:get_node("selected_x")
gui.set_alpha(self.selected_x, 0)
self.selected_y = self:get_node("selected_y")
gui.set_alpha(self.selected_y, 0)
self.selected_z = self:get_node("selected_z")
gui.set_alpha(self.selected_z, 0)
self.rich_input_x = self.druid:new_rich_input("rich_input_x")
self.rich_input_y = self.druid:new_rich_input("rich_input_y")
self.rich_input_z = self.druid:new_rich_input("rich_input_z")
self.value = vmath.vector3(0)
self.rich_input_x.input.on_input_unselect:subscribe(function()
self.value.x = tonumber(self.rich_input_x.input:get_text()) or 0
self.on_change:trigger(self.value)
end)
self.rich_input_y.input.on_input_unselect:subscribe(function()
self.value.y = tonumber(self.rich_input_y.input:get_text()) or 0
self.on_change:trigger(self.value)
end)
self.rich_input_z.input.on_input_unselect:subscribe(function()
self.value.z = tonumber(self.rich_input_z.input:get_text()) or 0
self.on_change:trigger(self.value)
end)
self.container = self.druid:new_container(self.root)
self.container:add_container("text_name")
self.container:add_container("E_Anchor")
self.on_change = event.create()
end
---@param text string
---@return widget.property_vector3
function M:set_text_property(text)
self.text_name:set_text(text)
return self
end
---@param x number
---@param y number
---@param z number
---@return widget.property_vector3
function M:set_value(x, y, z)
self.rich_input_x:set_text(tostring(x))
self.rich_input_y:set_text(tostring(y))
self.rich_input_z:set_text(tostring(z))
return self
end
return M

View File

@@ -0,0 +1,702 @@
fonts {
name: "druid_text_regular"
font: "/druid/fonts/druid_text_regular.font"
}
fonts {
name: "druid_text_bold"
font: "/druid/fonts/druid_text_bold.font"
}
textures {
name: "druid"
texture: "/druid/druid.atlas"
}
nodes {
size {
x: 400.0
y: 240.0
}
color {
x: 0.173
y: 0.184
z: 0.204
}
type: TYPE_BOX
texture: "druid/ui_circle_16"
id: "root"
inherit_alpha: true
slice9 {
x: 8.0
y: 8.0
z: 8.0
w: 8.0
}
}
nodes {
position {
y: 120.0
}
size {
x: 400.0
y: 40.0
}
type: TYPE_BOX
id: "header"
pivot: PIVOT_N
parent: "root"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
visible: false
}
nodes {
position {
x: -192.0
y: -8.0
}
scale {
x: 0.5
y: 0.5
}
size {
x: 500.0
y: 50.0
}
color {
x: 0.463
y: 0.475
z: 0.49
}
type: TYPE_TEXT
text: "Properties"
font: "druid_text_regular"
id: "text_header"
pivot: PIVOT_NW
outline {
x: 1.0
y: 1.0
z: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
}
parent: "header"
inherit_alpha: true
outline_alpha: 0.0
shadow_alpha: 0.0
}
nodes {
position {
x: 192.0
y: -4.0
}
color {
x: 0.306
y: 0.31
z: 0.314
}
type: TYPE_BOX
texture: "druid/icon_drag"
id: "icon_drag"
pivot: PIVOT_NE
parent: "header"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
}
nodes {
position {
y: -120.0
}
size {
x: 400.0
y: 190.0
}
type: TYPE_BOX
id: "content"
pivot: PIVOT_S
parent: "root"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
visible: false
}
nodes {
position {
x: -200.0
y: 190.0
}
size {
x: 400.0
y: 190.0
}
type: TYPE_BOX
texture: "druid/empty"
id: "scroll_view"
xanchor: XANCHOR_LEFT
pivot: PIVOT_NW
adjust_mode: ADJUST_MODE_STRETCH
parent: "content"
inherit_alpha: true
clipping_mode: CLIPPING_MODE_STENCIL
}
nodes {
size {
x: 400.0
y: 190.0
}
type: TYPE_BOX
texture: "druid/pixel"
id: "scroll_content"
pivot: PIVOT_NW
adjust_mode: ADJUST_MODE_STRETCH
parent: "scroll_view"
inherit_alpha: true
slice9 {
x: 8.0
y: 8.0
w: 6.0
}
visible: false
}
nodes {
position {
y: 170.0
}
type: TYPE_BOX
texture: "druid/empty"
id: "propeties"
parent: "content"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
visible: false
}
nodes {
type: TYPE_TEMPLATE
id: "property_slider"
parent: "propeties"
inherit_alpha: true
template: "/druid/widget/properties_panel/properties/property_slider.gui"
}
nodes {
type: TYPE_BOX
id: "property_slider/root"
parent: "property_slider"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_slider/text_name"
parent: "property_slider/root"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_slider/E_Anchor"
parent: "property_slider/root"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_slider/slider"
parent: "property_slider/E_Anchor"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_slider/slider_back"
parent: "property_slider/slider"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_slider/slider_pin"
parent: "property_slider/slider"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_slider/button"
parent: "property_slider/E_Anchor"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_slider/selected"
parent: "property_slider/button"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_slider/text_value"
parent: "property_slider/button"
template_node_child: true
}
nodes {
position {
y: -50.0
}
type: TYPE_TEMPLATE
id: "property_checkbox"
parent: "propeties"
inherit_alpha: true
template: "/druid/widget/properties_panel/properties/property_checkbox.gui"
}
nodes {
type: TYPE_BOX
id: "property_checkbox/root"
parent: "property_checkbox"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_checkbox/text_name"
parent: "property_checkbox/root"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_checkbox/E_Anchor"
parent: "property_checkbox/root"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_checkbox/button"
parent: "property_checkbox/E_Anchor"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_checkbox/icon"
parent: "property_checkbox/button"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_checkbox/selected"
parent: "property_checkbox/button"
template_node_child: true
}
nodes {
position {
y: -100.0
}
type: TYPE_TEMPLATE
id: "property_button"
parent: "propeties"
inherit_alpha: true
template: "/druid/widget/properties_panel/properties/property_button.gui"
}
nodes {
type: TYPE_BOX
id: "property_button/root"
parent: "property_button"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_button/text_name"
parent: "property_button/root"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_button/E_Anchor"
parent: "property_button/root"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_button/button"
parent: "property_button/E_Anchor"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_button/selected"
parent: "property_button/button"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_button/text_button"
parent: "property_button/button"
template_node_child: true
}
nodes {
position {
y: -150.0
}
type: TYPE_TEMPLATE
id: "property_input"
parent: "propeties"
inherit_alpha: true
template: "/druid/widget/properties_panel/properties/property_input.gui"
}
nodes {
type: TYPE_BOX
id: "property_input/root"
parent: "property_input"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_input/text_name"
parent: "property_input/root"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_input/E_Anchor"
parent: "property_input/root"
template_node_child: true
}
nodes {
type: TYPE_TEMPLATE
id: "property_input/rich_input"
parent: "property_input/E_Anchor"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_input/rich_input/root"
parent: "property_input/rich_input"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_input/rich_input/button"
parent: "property_input/rich_input/root"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_input/rich_input/placeholder_text"
parent: "property_input/rich_input/root"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_input/rich_input/input_text"
parent: "property_input/rich_input/root"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_input/rich_input/cursor_node"
parent: "property_input/rich_input/root"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_input/rich_input/cursor_text"
parent: "property_input/rich_input/cursor_node"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_input/selected"
parent: "property_input/E_Anchor"
template_node_child: true
}
nodes {
position {
y: -200.0
}
type: TYPE_TEMPLATE
id: "property_text"
parent: "propeties"
inherit_alpha: true
template: "/druid/widget/properties_panel/properties/property_text.gui"
}
nodes {
type: TYPE_BOX
id: "property_text/root"
parent: "property_text"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_text/text_name"
parent: "property_text/root"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_text/text_right"
parent: "property_text/root"
template_node_child: true
}
nodes {
position {
y: -250.0
}
type: TYPE_TEMPLATE
id: "property_left_right_selector"
parent: "propeties"
inherit_alpha: true
template: "/druid/widget/properties_panel/properties/property_left_right_selector.gui"
}
nodes {
type: TYPE_BOX
id: "property_left_right_selector/root"
parent: "property_left_right_selector"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_left_right_selector/text_name"
parent: "property_left_right_selector/root"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_left_right_selector/E_Anchor"
parent: "property_left_right_selector/root"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_left_right_selector/button_left"
parent: "property_left_right_selector/E_Anchor"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_left_right_selector/icon_left"
parent: "property_left_right_selector/button_left"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_left_right_selector/button_right"
parent: "property_left_right_selector/E_Anchor"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_left_right_selector/icon_right"
parent: "property_left_right_selector/button_right"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_left_right_selector/selected"
parent: "property_left_right_selector/E_Anchor"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_left_right_selector/text_value"
parent: "property_left_right_selector/E_Anchor"
template_node_child: true
}
nodes {
position {
y: -300.0
}
type: TYPE_TEMPLATE
id: "property_vector3"
parent: "propeties"
inherit_alpha: true
template: "/druid/widget/properties_panel/properties/property_vector3.gui"
}
nodes {
type: TYPE_BOX
id: "property_vector3/root"
parent: "property_vector3"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_vector3/text_name"
parent: "property_vector3/root"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_vector3/E_Anchor"
parent: "property_vector3/root"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_vector3/field_x"
parent: "property_vector3/E_Anchor"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_vector3/text_x"
parent: "property_vector3/field_x"
template_node_child: true
}
nodes {
type: TYPE_TEMPLATE
id: "property_vector3/rich_input_x"
parent: "property_vector3/field_x"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_vector3/rich_input_x/root"
parent: "property_vector3/rich_input_x"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_vector3/rich_input_x/button"
parent: "property_vector3/rich_input_x/root"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_vector3/rich_input_x/placeholder_text"
parent: "property_vector3/rich_input_x/root"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_vector3/rich_input_x/input_text"
parent: "property_vector3/rich_input_x/root"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_vector3/rich_input_x/cursor_node"
parent: "property_vector3/rich_input_x/root"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_vector3/rich_input_x/cursor_text"
parent: "property_vector3/rich_input_x/cursor_node"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_vector3/selected_x"
parent: "property_vector3/field_x"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_vector3/field_y"
parent: "property_vector3/E_Anchor"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_vector3/text_y"
parent: "property_vector3/field_y"
template_node_child: true
}
nodes {
type: TYPE_TEMPLATE
id: "property_vector3/rich_input_y"
parent: "property_vector3/field_y"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_vector3/rich_input_y/root"
parent: "property_vector3/rich_input_y"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_vector3/rich_input_y/button"
parent: "property_vector3/rich_input_y/root"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_vector3/rich_input_y/placeholder_text"
parent: "property_vector3/rich_input_y/root"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_vector3/rich_input_y/input_text"
parent: "property_vector3/rich_input_y/root"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_vector3/rich_input_y/cursor_node"
parent: "property_vector3/rich_input_y/root"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_vector3/rich_input_y/cursor_text"
parent: "property_vector3/rich_input_y/cursor_node"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_vector3/selected_y"
parent: "property_vector3/field_y"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_vector3/field_z"
parent: "property_vector3/E_Anchor"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_vector3/text_z"
parent: "property_vector3/field_z"
template_node_child: true
}
nodes {
type: TYPE_TEMPLATE
id: "property_vector3/rich_input_z"
parent: "property_vector3/field_z"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_vector3/rich_input_z/root"
parent: "property_vector3/rich_input_z"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_vector3/rich_input_z/button"
parent: "property_vector3/rich_input_z/root"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_vector3/rich_input_z/placeholder_text"
parent: "property_vector3/rich_input_z/root"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_vector3/rich_input_z/input_text"
parent: "property_vector3/rich_input_z/root"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_vector3/rich_input_z/cursor_node"
parent: "property_vector3/rich_input_z/root"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "property_vector3/rich_input_z/cursor_text"
parent: "property_vector3/rich_input_z/cursor_node"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "property_vector3/selected_z"
parent: "property_vector3/field_z"
template_node_child: true
}
material: "/builtins/materials/gui.material"
adjust_reference: ADJUST_REFERENCE_PARENT

View File

@@ -0,0 +1,319 @@
local property_checkbox = require("druid.widget.properties_panel.properties.property_checkbox")
local property_slider = require("druid.widget.properties_panel.properties.property_slider")
local property_button = require("druid.widget.properties_panel.properties.property_button")
local property_input = require("druid.widget.properties_panel.properties.property_input")
local property_text = require("druid.widget.properties_panel.properties.property_text")
local property_left_right_selector = require("druid.widget.properties_panel.properties.property_left_right_selector")
local property_vector3 = require("druid.widget.properties_panel.properties.property_vector3")
---@class widget.properties_panel: druid.widget
---@field root node
---@field scroll druid.scroll
---@field layout druid.layout
---@field container druid.container
---@field container_content druid.container
---@field container_scroll_view druid.container
---@field contaienr_scroll_content druid.container
---@field text_header druid.text
---@field paginator widget.property_left_right_selector
---@field properties druid.widget[] List of created properties
---@field properties_constructors fun()[] List of properties functions to create a new widget. Used to not spawn non-visible widgets but keep the reference
local M = {}
function M:init()
self.root = self:get_node("root")
self.content = self:get_node("content")
self.container = self.druid:new_container(self.root)
self.container:add_container("header")
self.container_content = self.container:add_container("content")
self.container_scroll_view = self.container_content:add_container("scroll_view")
self.contaienr_scroll_content = self.container_scroll_view:add_container("scroll_content")
self.default_size = self.container:get_size()
self.properties = {}
self.properties_constructors = {}
self.current_page = 1
self.properties_per_page = 15
self.text_header = self.druid:new_text("text_header")
self.scroll = self.druid:new_scroll("scroll_view", "scroll_content")
self.layout = self.druid:new_layout("scroll_content", "vertical")
:set_hug_content(false, true)
:set_padding(nil, 0)
self.layout.on_size_changed:subscribe(self.on_size_changed, self)
self.druid:new_drag("header", self.on_drag_widget)
self.druid:new_button("icon_drag", self.toggle_hide)
:set_style(nil)
self.property_checkbox_prefab = self:get_node("property_checkbox/root")
gui.set_enabled(self.property_checkbox_prefab, false)
self.property_slider_prefab = self:get_node("property_slider/root")
gui.set_enabled(self.property_slider_prefab, false)
self.property_button_prefab = self:get_node("property_button/root")
gui.set_enabled(self.property_button_prefab, false)
self.property_input_prefab = self:get_node("property_input/root")
gui.set_enabled(self.property_input_prefab, false)
self.property_text_prefab = self:get_node("property_text/root")
gui.set_enabled(self.property_text_prefab, false)
self.property_left_right_selector_prefab = self:get_node("property_left_right_selector/root")
gui.set_enabled(self.property_left_right_selector_prefab, false)
self.property_vector3_prefab = self:get_node("property_vector3/root")
gui.set_enabled(self.property_vector3_prefab, false)
-- We not using as a part of properties, since it handled in a way to be paginable
self.paginator = self.druid:new_widget(property_left_right_selector, "property_left_right_selector", self.property_left_right_selector_prefab)
self.paginator:set_text("Page")
self.paginator:set_number_type(1, 1, true)
self.paginator:set_value(self.current_page)
self.paginator.on_change_value:subscribe(function(value)
self:set_page(value)
end)
local width = self.layout:get_content_size()
self.paginator.container:set_size(width)
gui.set_enabled(self.paginator.root, false)
end
function M:on_remove()
self:clear()
end
function M:on_drag_widget(dx, dy)
local position = self.container:get_position()
self.container:set_position(position.x + dx, position.y + dy)
end
function M:clear_created_properties()
for index = 1, #self.properties do
local property = self.properties[index]
-- If prefab used clone nodes we can remove it
if property:get_nodes() then
gui.delete_node(property.root)
else
-- Probably we have component placed on scene directly
gui.set_enabled(property.root, false)
end
self.druid:remove(self.properties[index])
end
self.properties = {}
self.layout:clear_layout()
-- Use paginator as "pinned" widget
self.layout:add(self.paginator.root)
end
function M:clear()
self:clear_created_properties()
self.properties_constructors = {}
end
function M:on_size_changed(new_size)
self.container_content:set_size(new_size.x, new_size.y, gui.PIVOT_N)
self.default_size = vmath.vector3(new_size.x, new_size.y + 50, 0)
if not self.is_hidden then
self.container:set_size(self.default_size.x, self.default_size.y, gui.PIVOT_N)
end
local width = self.layout:get_size().x - self.layout.padding.x - self.layout.padding.z
for index = 1, #self.properties do
local property = self.properties[index]
if property.container then
property.container:set_size(width)
end
end
self.paginator.container:set_size(width)
end
function M:update(dt)
if self.is_dirty then
self.is_dirty = false
self:clear_created_properties()
local properties_count = #self.properties_constructors
-- Render all current properties
local start_index = (self.current_page - 1) * self.properties_per_page + 1
local end_index = start_index + self.properties_per_page - 1
end_index = math.min(end_index, properties_count)
local is_paginator_visible = properties_count > self.properties_per_page
gui.set_enabled(self.paginator.root, is_paginator_visible)
self.paginator:set_number_type(1, math.ceil(properties_count / self.properties_per_page), true)
self.paginator.text_value:set_text(self.current_page .. " / " .. math.ceil(properties_count / self.properties_per_page))
for index = start_index, end_index do
self.properties_constructors[index]()
end
end
end
---@param on_create fun(checkbox: widget.property_checkbox)|nil
---@return widget.properties_panel
function M:add_checkbox(on_create)
return self:add_inner_widget(property_checkbox, "property_checkbox", self.property_checkbox_prefab, on_create)
end
---@param on_create fun(slider: widget.property_slider)|nil
---@return widget.properties_panel
function M:add_slider(on_create)
return self:add_inner_widget(property_slider, "property_slider", self.property_slider_prefab, on_create)
end
---@param on_create fun(button: widget.property_button)|nil
---@return widget.properties_panel
function M:add_button(on_create)
return self:add_inner_widget(property_button, "property_button", self.property_button_prefab, on_create)
end
---@param on_create fun(input: widget.property_input)|nil
---@return widget.properties_panel
function M:add_input(on_create)
return self:add_inner_widget(property_input, "property_input", self.property_input_prefab, on_create)
end
---@param on_create fun(text: widget.property_text)|nil
function M:add_text(on_create)
return self:add_inner_widget(property_text, "property_text", self.property_text_prefab, on_create)
end
---@param on_create fun(selector: widget.property_left_right_selector)|nil
function M:add_left_right_selector(on_create)
return self:add_inner_widget(property_left_right_selector, "property_left_right_selector", self.property_left_right_selector_prefab, on_create)
end
---@param on_create fun(vector3: widget.property_vector3)|nil
function M:add_vector3(on_create)
return self:add_inner_widget(property_vector3, "property_vector3", self.property_vector3_prefab, on_create)
end
---@generic T: druid.widget
---@param widget_class T
---@param template string|nil
---@param nodes table<hash, node>|node|nil
---@param on_create fun(widget: T)|nil
---@return widget.properties_panel
function M:add_inner_widget(widget_class, template, nodes, on_create)
table.insert(self.properties_constructors, function()
local widget = self.druid:new_widget(widget_class, template, nodes)
self:add_property(widget)
if on_create then
on_create(widget)
end
end)
self.is_dirty = true
return self
end
---@param create_widget_callback fun(): druid.widget
---@return widget.properties_panel
function M:add_widget(create_widget_callback)
table.insert(self.properties_constructors, function()
local widget = create_widget_callback()
self:add_property(widget)
end)
self.is_dirty = true
return self
end
---@private
function M:create_from_prefab(widget_class, template, nodes)
return self:add_property(self.druid:new_widget(widget_class, template, nodes))
end
---@private
function M:add_property(widget)
gui.set_enabled(widget.root, true)
table.insert(self.properties, widget)
local width = self.layout:get_content_size()
widget.container:set_size(width)
self.layout:add(widget.root)
return widget
end
function M:remove(widget)
for index = 1, #self.properties do
if self.properties[index] == widget then
self.druid:remove(widget)
self.layout:remove(widget.root)
-- If prefab used clone nodes we can remove it
if widget:get_nodes() then
gui.delete_node(widget.root)
else
-- Probably we have component placed on scene directly
gui.set_enabled(widget.root, false)
end
table.remove(self.properties, index)
break
end
end
end
function M:toggle_hide()
self.is_hidden = not self.is_hidden
local hidden_size = gui.get_size(self:get_node("header"))
local new_size = self.is_hidden and hidden_size or self.default_size
self.container:set_size(new_size.x, new_size.y, gui.PIVOT_N)
gui.set_enabled(self.content, not self.is_hidden)
return self
end
---@param properties_per_page number
function M:set_properties_per_page(properties_per_page)
self.properties_per_page = properties_per_page
end
function M:set_page(page)
self.current_page = page
self.is_dirty = true
end
return M

View File

@@ -1,4 +1,3 @@
script: ""
fonts { fonts {
name: "text_regular" name: "text_regular"
font: "/example/assets/fonts/text_regular.font" font: "/example/assets/fonts/text_regular.font"
@@ -8,434 +7,129 @@ textures {
texture: "/example/assets/druid_logo.atlas" texture: "/example/assets/druid_logo.atlas"
} }
textures { textures {
name: "druid" name: "druid_example"
texture: "/example/assets/druid.atlas" texture: "/example/assets/druid_example.atlas"
}
background_color {
x: 0.0
y: 0.0
z: 0.0
w: 0.0
} }
nodes { nodes {
position {
x: 0.0
y: 0.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
}
size { size {
x: 400.0 x: 400.0
y: 170.0 y: 170.0
z: 0.0
w: 1.0
} }
color { color {
x: 0.129 x: 0.129
y: 0.141 y: 0.141
z: 0.157 z: 0.157
w: 1.0
} }
type: TYPE_BOX type: TYPE_BOX
blend_mode: BLEND_MODE_ALPHA texture: "druid_example/pixel"
texture: "druid/pixel"
id: "root" id: "root"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
adjust_mode: ADJUST_MODE_FIT
layer: "druid" layer: "druid"
inherit_alpha: true inherit_alpha: true
slice9 {
x: 0.0
y: 0.0
z: 0.0
w: 0.0
}
clipping_mode: CLIPPING_MODE_STENCIL clipping_mode: CLIPPING_MODE_STENCIL
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 { nodes {
position { position {
x: 200.0 x: 200.0
y: 0.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
} }
size { size {
x: 16.0 x: 16.0
y: 16.0 y: 16.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
} }
type: TYPE_BOX type: TYPE_BOX
blend_mode: BLEND_MODE_ALPHA
texture: ""
id: "E_Anchor" id: "E_Anchor"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_E pivot: PIVOT_E
adjust_mode: ADJUST_MODE_STRETCH adjust_mode: ADJUST_MODE_STRETCH
parent: "root" parent: "root"
layer: ""
inherit_alpha: true 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: false visible: false
material: ""
} }
nodes { nodes {
position { position {
x: 10.0 x: 10.0
y: 0.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
}
size {
x: 200.0
y: 100.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
} }
type: TYPE_BOX type: TYPE_BOX
blend_mode: BLEND_MODE_ALPHA
texture: "druid_logo/icon_druid" texture: "druid_logo/icon_druid"
id: "icon_druid_right" id: "icon_druid_right"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
adjust_mode: ADJUST_MODE_FIT
parent: "E_Anchor" parent: "E_Anchor"
layer: "druid_logo" layer: "druid_logo"
inherit_alpha: true 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: 0.5 alpha: 0.5
template_node_child: false
size_mode: SIZE_MODE_AUTO size_mode: SIZE_MODE_AUTO
custom_type: 0
enabled: true
visible: true
material: ""
} }
nodes { nodes {
position { position {
x: -200.0 x: -200.0
y: 0.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
} }
size { size {
x: 16.0 x: 16.0
y: 16.0 y: 16.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
} }
type: TYPE_BOX type: TYPE_BOX
blend_mode: BLEND_MODE_ALPHA
texture: ""
id: "W_Anchor" id: "W_Anchor"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_W pivot: PIVOT_W
adjust_mode: ADJUST_MODE_STRETCH adjust_mode: ADJUST_MODE_STRETCH
parent: "root" parent: "root"
layer: ""
inherit_alpha: true 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: false visible: false
material: ""
} }
nodes { nodes {
position { position {
x: -10.0 x: -10.0
y: 0.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
}
size {
x: 200.0
y: 100.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
} }
type: TYPE_BOX type: TYPE_BOX
blend_mode: BLEND_MODE_ALPHA
texture: "druid_logo/icon_druid" texture: "druid_logo/icon_druid"
id: "icon_druid_left" id: "icon_druid_left"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
adjust_mode: ADJUST_MODE_FIT
parent: "W_Anchor" parent: "W_Anchor"
layer: "druid_logo" layer: "druid_logo"
inherit_alpha: true 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: 0.5 alpha: 0.5
template_node_child: false
size_mode: SIZE_MODE_AUTO size_mode: SIZE_MODE_AUTO
custom_type: 0
enabled: true
visible: true
material: ""
} }
nodes { nodes {
position {
x: 0.0
y: 0.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
}
size {
x: 200.0
y: 100.0
z: 0.0
w: 1.0
}
color {
x: 1.0
y: 1.0
z: 1.0
w: 1.0
}
type: TYPE_BOX type: TYPE_BOX
blend_mode: BLEND_MODE_ALPHA
texture: "druid_logo/logo_druid" texture: "druid_logo/logo_druid"
id: "icon_logo" id: "icon_logo"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
adjust_mode: ADJUST_MODE_FIT
parent: "root" parent: "root"
layer: "druid_logo" layer: "druid_logo"
inherit_alpha: true 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_AUTO size_mode: SIZE_MODE_AUTO
custom_type: 0
enabled: true
visible: true
material: ""
} }
nodes { nodes {
position { position {
x: 0.0
y: -50.0 y: -50.0
z: 0.0
w: 1.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
} }
scale { scale {
x: 0.6 x: 0.6
y: 0.6 y: 0.6
z: 1.0
w: 1.0
} }
size { size {
x: 400.0 x: 400.0
y: 50.0 y: 50.0
z: 0.0
w: 1.0
} }
color { color {
x: 0.463 x: 0.463
y: 0.475 y: 0.475
z: 0.49 z: 0.49
w: 1.0
} }
type: TYPE_TEXT type: TYPE_TEXT
blend_mode: BLEND_MODE_ALPHA
text: "Defold UI Framework" text: "Defold UI Framework"
font: "text_regular" font: "text_regular"
id: "text_description" id: "text_description"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
outline { outline {
x: 1.0 x: 1.0
y: 1.0 y: 1.0
z: 1.0 z: 1.0
w: 1.0
} }
shadow { shadow {
x: 1.0 x: 1.0
y: 1.0 y: 1.0
z: 1.0 z: 1.0
w: 1.0
} }
adjust_mode: ADJUST_MODE_FIT
line_break: false
parent: "root" parent: "root"
layer: "text_regular" layer: "text_regular"
inherit_alpha: true inherit_alpha: true
alpha: 1.0
outline_alpha: 0.0 outline_alpha: 0.0
shadow_alpha: 0.0 shadow_alpha: 0.0
template_node_child: false
text_leading: 1.0
text_tracking: 0.0
custom_type: 0
enabled: true
visible: true
material: ""
} }
layers { layers {
name: "druid" name: "druid"
@@ -448,4 +142,3 @@ layers {
} }
material: "/builtins/materials/gui.material" material: "/builtins/materials/gui.material"
adjust_reference: ADJUST_REFERENCE_PARENT adjust_reference: ADJUST_REFERENCE_PARENT
max_nodes: 512

View File

@@ -2,7 +2,6 @@ local panthera = require("panthera.panthera")
local component = require("druid.component") local component = require("druid.component")
local druid_logo_panthera = require("example.components.druid_logo.druid_logo_panthera") local druid_logo_panthera = require("example.components.druid_logo.druid_logo_panthera")
local container = require("example.components.container.container")
---@class druid_logo: druid.base_component ---@class druid_logo: druid.base_component
---@field root druid.container ---@field root druid.container
@@ -16,7 +15,7 @@ local DruidLogo = component.create("druid_logo")
function DruidLogo:init(template, nodes) function DruidLogo:init(template, nodes)
self.druid = self:get_druid(template, nodes) self.druid = self:get_druid(template, nodes)
self.root = self.druid:new(container, "root") --[[@as druid.container]] self.root = self.druid:new_container("root") --[[@as druid.container]]
self.root:add_container("E_Anchor") self.root:add_container("E_Anchor")
self.root:add_container("W_Anchor") self.root:add_container("W_Anchor")

Some files were not shown because too many files have changed in this diff Show More