diff --git a/docs/modules/druid.drag.html b/docs/modules/druid.drag.html new file mode 100644 index 0000000..ae4e709 --- /dev/null +++ b/docs/modules/druid.drag.html @@ -0,0 +1,286 @@ + + + + + Defold Druid UI Library + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Module druid.drag

+

Component to handle drag action on node.

+

Drag have correct handling for multitouch and swap + touched while dragging. Drag will be processed even + the cursor is outside of node, if drag is already started

+ + +

Functions

+ + + + + + + + + +
init(node, on_drag_callback)Drag component constructor
set_click_zone(zone)Strict drag click area.
+

Tables

+ + + + + + + + + + + + + +
EventsComponent events
FieldsComponents fields
StyleComponent style params
+ +
+
+ + +

Functions

+ +
+
+ + init(node, on_drag_callback) +
+
+ Drag component constructor + + +

Parameters:

+
    +
  • node + node + GUI node to detect dragging +
  • +
  • on_drag_callback + function + Callback for ondragevent(self, dx, dy) +
  • +
+ + + + + +
+
+ + set_click_zone(zone) +
+
+ Strict drag click area. Useful for + restrict events outside stencil node + + +

Parameters:

+
    +
  • zone + node + Gui node +
  • +
+ + + + + +
+
+

Tables

+ +
+
+ + Events +
+
+ Component events + + +

Fields:

+
    +
  • on_touch_start + druid_event + (self) Event on touch start +
  • +
  • on_touch_end + druid_event + (self) Event on touch end +
  • +
  • on_drag_start + druid_event + (self) Event on drag start +
  • +
  • on_drag + druid_event + (self, dx, dy) Event on drag progress +
  • +
  • on_drag_end + druid_event + (self) Event on drag end +
  • +
+ + + + + +
+
+ + Fields +
+
+ Components fields + + +

Fields:

+
    +
  • is_touch + bool + Is component now touching +
  • +
  • is_drag + bool + Is component now dragging +
  • +
  • can_x + bool + Is drag component process vertical dragging. Default - true +
  • +
  • can_y + bool + Is drag component process horizontal. Default - true +
  • +
  • x + number + Current touch x position +
  • +
  • y + number + Current touch y position +
  • +
  • touch_start_pos + vector3 + Touch start position +
  • +
+ + + + + +
+
+ + Style +
+
+ Component style params + + +

Fields:

+
    +
  • DRAG_DEADZONE + number + Distance in pixels to start dragging +
  • +
+ + + + + +
+
+ + +
+
+
+generated by LDoc 1.4.6 +Last updated 2020-05-04 00:06:50 +
+
+ + diff --git a/docs_md/changelog.md b/docs_md/changelog.md index 0959b59..bf0018e 100644 --- a/docs_md/changelog.md +++ b/docs_md/changelog.md @@ -39,4 +39,37 @@ Druid 0.3.0: - Known issues: - Adjusting text size by height works wrong. Adjusting single line texting works fine - Space is not working in HTML5 - \ No newline at end of file + + + +Druid 0.4.0: + +- Add _Drag_ basic component + - Drag component allow you detect dragging on GUI node + - Drag will be processed even the cursor is outside of node, if drag is already started + - Drag provides correct handle of several touches. Drag can switch between them (no more scroll gliches with position) + - Drag have next events: + - on_touch_start (self) + - on_touch_end (self) + - on_drag_start (self) + - on_drag (self, dx, dy) + - on_drag_end (self) + - You can restriction side of draggin by changing _drag.can_x_ and _drag.can_y_ fields + - You can setup drag deadzone to detect, when dragging is started (by default 10 pixels) + +- Druid _Scroll_ component fully reworked. Input logic moved to _Drag_ component + - Updated scroll documentation + - Changed constructor order params + - Scroll now contains from view and content node + - _View node_ - static node, which size determine the "camera" zone + - _Content node_ - dynamic node, moving by _Scroll_ component + - Scroll will be disabled only if content size equals to view size (by width or height separatly) + - You can adjust scroll size via _.gui_ scene. Just setup correct node size + - Different anchoring is supported (for easier layout) + - Function _scroll_to_ now accept position relative to _content node_. It's more easier for handling. _Example:_ if you have children node of _content_node_, you can pass this node position to scroll to this. + - **Resolve #52**: _Content node size_ now can be less than _view node size_. In this case, content will be scrolled only inside _view size_ + - **Fix #50**: If style:SOFT_ZONE_SIZE equals to [0..1], scroll can be disappeared + +- _Grid_ anchor by default equals to node pivot (so, more gui settings in _.gui_ settings) + +- **Fix:** Blocker component bug (blocker had very high priority, so it's block even button components, created after bloker) \ No newline at end of file diff --git a/druid/base/blocker.lua b/druid/base/blocker.lua index 0b336bd..e5c3931 100644 --- a/druid/base/blocker.lua +++ b/druid/base/blocker.lua @@ -14,7 +14,7 @@ local Event = require("druid.event") local const = require("druid.const") local component = require("druid.component") -local M = component.create("blocker", { const.ON_INPUT_HIGH }) +local M = component.create("blocker", { const.ON_INPUT }) --- Component init function @@ -29,7 +29,7 @@ end function M.on_input(self, action_id, action) - if action_id ~= const.ACTION_TOUCH then + if action_id ~= const.ACTION_TOUCH and action_id ~= const.ACTION_MULTITOUCH then return false end diff --git a/druid/base/drag.lua b/druid/base/drag.lua index e8205c3..8e1199a 100644 --- a/druid/base/drag.lua +++ b/druid/base/drag.lua @@ -1,14 +1,30 @@ ---- Component to handle drag action on node +--- Component to handle drag action on node. +-- Drag have correct handling for multitouch and swap +-- touched while dragging. Drag will be processed even +-- the cursor is outside of node, if drag is already started -- @module druid.drag ---- Components fields --- @table Fields - --- Component events -- @table Events +-- @tfield druid_event on_touch_start (self) Event on touch start +-- @tfield druid_event on_touch_end (self) Event on touch end +-- @tfield druid_event on_drag_start (self) Event on drag start +-- @tfield druid_event on_drag (self, dx, dy) Event on drag progress +-- @tfield druid_event on_drag_end (self) Event on drag end + +--- Components fields +-- @table Fields +-- @tfield bool is_touch Is component now touching +-- @tfield bool is_drag Is component now dragging +-- @tfield bool can_x Is drag component process vertical dragging. Default - true +-- @tfield bool can_y Is drag component process horizontal. Default - true +-- @tfield number x Current touch x position +-- @tfield number y Current touch y position +-- @tfield vector3 touch_start_pos Touch start position --- Component style params -- @table Style +-- @tfield number DRAG_DEADZONE Distance in pixels to start dragging local Event = require("druid.event") local const = require("druid.const") @@ -49,12 +65,13 @@ local function process_touch(self, touch) if not self.can_x then self.touch_start_pos.x = touch.x end + if not self.can_y then self.touch_start_pos.y = touch.y end local distance = helper.distance(touch.x, touch.y, self.touch_start_pos.x, self.touch_start_pos.y) - if not self.is_drag and distance >= self.drag_deadzone then + if not self.is_drag and distance >= self.style.DRAG_DEADZONE then self.is_drag = true self.on_drag_start:trigger(self:get_context()) self:increase_input_priority() @@ -62,6 +79,8 @@ local function process_touch(self, touch) end +--- Return current touch action from action input data +-- If touch_id stored - return exact this touch action local function find_touch(action_id, action, touch_id) local act = helper.is_mobile() and const.ACTION_MULTITOUCH or const.ACTION_TOUCH @@ -83,6 +102,8 @@ local function find_touch(action_id, action, touch_id) end +--- Process on touch release. We should to find, if any other +-- touches exists to switch to another touch. local function on_touch_release(self, action_id, action) if #action.touch >= 2 then -- Find next unpressed touch @@ -107,14 +128,14 @@ local function on_touch_release(self, action_id, action) end ---- Component init function +--- Drag component constructor +-- @tparam node node GUI node to detect dragging +-- @tparam function on_drag_callback Callback for on_drag_event(self, dx, dy) -- @function drag:init function M.init(self, node, on_drag_callback) self.style = self:get_style() self.node = self:get_node(node) - self.drag_deadzone = self.style.DRAG_DEADZONE or 10 - self.dx = 0 self.dy = 0 self.touch_id = 0 @@ -162,8 +183,6 @@ function M.on_input(self, action_id, action) return false end - self.dx = 0 - self.dy = 0 local touch = find_touch(action_id, action, self.touch_id) if not touch then @@ -174,6 +193,9 @@ function M.on_input(self, action_id, action) self.touch_id = touch.id end + self.dx = 0 + self.dy = 0 + if touch.pressed and not self.is_touch then start_touch(self, touch) end diff --git a/druid/base/scroll.lua b/druid/base/scroll.lua index f48a009..dd2a534 100644 --- a/druid/base/scroll.lua +++ b/druid/base/scroll.lua @@ -1,14 +1,40 @@ ---- +--- Component to handle scroll content. +-- Scroll consist from two nodes: scroll parent and scroll input +-- Scroll input the user input zone, it's static +-- Scroll parent the scroll moving part, it will change position. +-- Setup initial scroll size by changing scroll parent size. If scroll parent +-- size will be less than scroll_input size, no scroll is available. For scroll +-- parent size should be more than input size -- @module druid.scroll ---- Components fields --- @table Fields - --- Component events -- @table Events +-- @tfield druid_event on_scroll On scroll move callback +-- @tfield druid_event on_scroll_to On scroll_to function callback +-- @tfield druid_event on_point_scroll On scroll_to_index function callback + +--- Component fields +-- @table Fields +-- @tfield node node Scroll parent node +-- @tfield node input_zone Scroll input node +-- @tfield vector3 zone_size Current scroll content size +-- @tfield number soft_size Soft zone size from style table +-- @tfield vector3 center_offset Distance from node to node's center +-- @tfield bool is_inert Flag, if scroll now moving by inertion +-- @tfield vector3 inert Current inert speed +-- @tfield vector3 pos Current scroll posisition +-- @tfield vector3 target Current scroll target position --- Component style params -- @table Style +-- @tfield number FRICT_HOLD Multiplier for inertion, while touching +-- @tfield number FRICT Multiplier for free inertion +-- @tfield number INERT_THRESHOLD Scroll speed to stop inertion +-- @tfield number INERT_SPEED Multiplier for inertion speed +-- @tfield number DEADZONE Deadzone for start scrol in pixels +-- @tfield number SOFT_ZONE_SIZE Size of outside zone in pixels (for scroll back moving) +-- @tfield number BACK_SPEED Scroll back returning lerp speed +-- @tfield number ANIM_SPEED Scroll gui.animation speed for scroll_to function local Event = require("druid.event") local const = require("druid.const") @@ -23,8 +49,29 @@ local function inverse_lerp(min, max, current) end +--- Update vector with next conditions: +-- Field x have to <= field z +-- Field y have to <= field w +local function get_border_vector(vector) + if vector.x > vector.z then + vector.x, vector.z = vector.z, vector.x + end + if vector.y > vector.w then + vector.y, vector.w = vector.w, vector.y + end + + return vector +end + + +--- Return size from scroll border vector4 +local function get_size_vector(vector) + return vmath.vector3(vector.z - vector.x, vector.w - vector.y, 0) +end + + local function on_scroll_drag(self, dx, dy) - local t = self.target_pos + local t = self.target_position local b = self.available_pos local eb = self.available_pos_extra @@ -64,50 +111,54 @@ local function on_scroll_drag(self, dx, dy) end -local function set_pos(self, position) - position.x = helper.clamp(position.x, self.available_pos_extra.x, self.available_pos_extra.z) - position.y = helper.clamp(position.y, self.available_pos_extra.w, self.available_pos_extra.y) - - if self.current_pos.x ~= position.x or self.current_pos.y ~= position.y then - self.current_pos.x = position.x - self.current_pos.y = position.y - gui.set_position(self.content_node, position) - - self.on_scroll:trigger(self:get_context(), self.current_pos) - end -end - - -local function update_hand_scroll(self, dt) - local dx = self.target_pos.x - self.current_pos.x - local dy = self.target_pos.y - self.current_pos.y - - self.inertion.x = (self.inertion.x + dx) * self.style.FRICT_HOLD - self.inertion.y = (self.inertion.y + dy) * self.style.FRICT_HOLD - - set_pos(self, self.target_pos) -end - - local function check_soft_zone(self) - local t = self.target_pos - local b = self.available_pos + local target = self.target_position + local border = self.available_pos + local speed = self.style.BACK_SPEED -- Right border (minimum x) - if t.x < b.x then - t.x = helper.step(t.x, b.x, math.abs(t.x - b.x) * self.style.BACK_SPEED) + if target.x < border.x then + target.x = helper.step(target.x, border.x, math.abs(target.x - border.x) * speed) end -- Left border (maximum x) - if t.x > b.z then - t.x = helper.step(t.x, b.z, math.abs(t.x - b.z) * self.style.BACK_SPEED) + if target.x > border.z then + target.x = helper.step(target.x, border.z, math.abs(target.x - border.z) * speed) end -- Top border (maximum y) - if t.y < b.y then - t.y = helper.step(t.y, b.y, math.abs(t.y - b.y) * self.style.BACK_SPEED) + if target.y < border.y then + target.y = helper.step(target.y, border.y, math.abs(target.y - border.y) * speed) end -- Bot border (minimum y) - if t.y > b.w then - t.y = helper.step(t.y, b.w, math.abs(t.y - b.w) * self.style.BACK_SPEED) + if target.y > border.w then + target.y = helper.step(target.y, border.w, math.abs(target.y - border.w) * speed) + end +end + + +--- Cancel animation on other animation or input touch +local function cancel_animate(self) + if self.is_animate then + self.target_position = gui.get_position(self.content_node) + self.position.x = self.target_position.x + self.position.y = self.target_position.y + gui.cancel_animation(self.content_node, gui.PROP_POSITION) + self.is_animate = false + end +end + + + +local function set_scroll_position(self, position) + local available_extra = self.available_pos_extra + position.x = helper.clamp(position.x, available_extra.x, available_extra.z) + position.y = helper.clamp(position.y, available_extra.w, available_extra.y) + + if self.position.x ~= position.x or self.position.y ~= position.y then + self.position.x = position.x + self.position.y = position.y + gui.set_position(self.content_node, position) + + self.on_scroll:trigger(self:get_context(), self.position) end end @@ -122,7 +173,7 @@ local function check_points(self) end local inert = self.inertion - if not self.is_inert then + if not self._is_inert then if math.abs(inert.x) > self.style.DEADZONE then self:scroll_to_index(self.selected - helper.sign(inert.x)) return @@ -140,7 +191,7 @@ local function check_points(self) local temp_dist_on_inert = math.huge local index = false local index_on_inert = false - local pos = self.current_pos + local pos = self.position for i = 1, #self.points do local p = self.points[i] @@ -180,19 +231,19 @@ local function check_threshold(self) self.inertion.y = 0 end - if is_stopped or not self.is_inert then + if is_stopped or not self._is_inert then check_points(self) end end local function update_free_scroll(self, dt) - local target = self.target_pos + local target = self.target_position - if self.is_inert and (self.inertion.x ~= 0 or self.inertion.y ~= 0) then + if self._is_inert and (self.inertion.x ~= 0 or self.inertion.y ~= 0) then -- Inertion apply - target.x = self.current_pos.x + self.inertion.x * self.style.INERT_SPEED * dt - target.y = self.current_pos.y + self.inertion.y * self.style.INERT_SPEED * dt + target.x = self.position.x + self.inertion.x * self.style.INERT_SPEED * dt + target.y = self.position.y + self.inertion.y * self.style.INERT_SPEED * dt check_threshold(self) end @@ -200,37 +251,29 @@ local function update_free_scroll(self, dt) -- Inertion friction self.inertion = self.inertion * self.style.FRICT - check_soft_zone(self) - set_pos(self, target) + if self.position.x ~= target.x or self.position.y ~= target.y then + check_soft_zone(self) + set_scroll_position(self, target) + end +end + + +local function update_hand_scroll(self, dt) + local dx = self.target_position.x - self.position.x + local dy = self.target_position.y - self.position.y + + self.inertion.x = (self.inertion.x + dx) * self.style.FRICT_HOLD + self.inertion.y = (self.inertion.y + dy) * self.style.FRICT_HOLD + + set_scroll_position(self, self.target_position) end local function on_touch_start(self) self.inertion.x = 0 self.inertion.y = 0 - self.target_pos.x = self.current_pos.x - self.target_pos.y = self.current_pos.y -end - - ---- Verify vector --- Field x have to <= field z --- Field y have to <= field w -local function verify_scroll_vector4(vector) - if vector.x > vector.z then - vector.x, vector.z = vector.z, vector.x - end - if vector.y > vector.w then - vector.y, vector.w = vector.w, vector.y - end - - return vector -end - - ---- Return size from scroll border vector4 -local function get_size_vector(vector) - return vmath.vector3(vector.z - vector.x, vector.w - vector.y, 0) + self.target_position.x = self.position.x + self.target_position.y = self.position.y end @@ -246,14 +289,17 @@ local function update_size(self) local content_border = helper.get_border(self.content_node) local content_size = vmath.mul_per_elem(gui.get_size(self.content_node), gui.get_scale(self.content_node)) - self.available_pos = verify_scroll_vector4(view_border - content_border) + self.available_pos = get_border_vector(view_border - content_border) self.available_size = get_size_vector(self.available_pos) self.drag.can_x = self.available_size.x > 0 self.drag.can_y = self.available_size.y > 0 - --== Extra content size calculation + -- Extra content size calculation + -- We add extra size only if scroll is available + -- Even the content zone size less than view zone size local content_border_extra = helper.get_border(self.content_node) + if self.drag.can_x then local sign = content_size.x > view_size.x and 1 or -1 content_border_extra.x = content_border_extra.x - self.extra_stretch_size * sign @@ -266,27 +312,20 @@ local function update_size(self) content_border_extra.w = content_border_extra.w - self.extra_stretch_size * sign end - self.available_pos_extra = verify_scroll_vector4(view_border - content_border_extra) + if not self.style.SMALL_CONTENT_SCROLL then + self.drag.can_x = content_size.x > view_size.x + self.drag.can_y = content_size.y > view_size.y + end + + self.available_pos_extra = get_border_vector(view_border - content_border_extra) self.available_size_extra = get_size_vector(self.available_pos_extra) end ---- Cancel animation on other animation or input touch -local function cancel_animate(self) - if self.animate then - self.target_pos = gui.get_position(self.content_node) - self.current_pos.x = self.target_pos.x - self.current_pos.y = self.target_pos.y - gui.cancel_animation(self.content_node, gui.PROP_POSITION) - self.animate = false - end -end - - ---- Component init function --- @function swipe:init --- @tparam node node Gui node --- @tparam function on_swipe_callback Swipe callback for on_swipe_end event +--- Scroll constructor. +-- @function scroll:init +-- @tparam node view_node GUI view scroll node +-- @tparam node content_node GUI content scroll node function M.init(self, view_zone, content_zone) self.druid = self:get_druid() self.style = self:get_style() @@ -294,10 +333,9 @@ function M.init(self, view_zone, content_zone) self.view_node = self:get_node(view_zone) self.content_node = self:get_node(content_zone) - self.current_pos = gui.get_position(self.content_node) - self.target_pos = vmath.vector3(self.current_pos) + self.position = gui.get_position(self.content_node) + self.target_position = vmath.vector3(self.position) self.inertion = vmath.vector3(0) - self.extra_stretch_size = self.style.EXTRA_STRECH_SIZE self.drag = self.druid:new_drag(view_zone, on_scroll_drag) self.drag.on_touch_start:subscribe(on_touch_start) @@ -307,18 +345,15 @@ function M.init(self, view_zone, content_zone) self.on_scroll_to = Event() self.on_point_scroll = Event() - self.is_inert = true + self.selected = nil + self._is_inert = true + self.is_animate = false + self.extra_stretch_size = self.style.EXTRA_STRECH_SIZE update_size(self) end -function M.set_size(self, size) - gui.set_size(self.content_node, size) - update_size(self) -end - - function M.update(self, dt) if self.drag.is_drag then update_hand_scroll(self, dt) @@ -328,10 +363,10 @@ function M.update(self, dt) end ---- Start scroll to target point +--- Start scroll to target point. -- @function scroll:scroll_to --- @tparam point vector3 target point --- @tparam[opt] bool is_instant instant scroll flag +-- @tparam point vector3 Target point +-- @tparam[opt] bool is_instant Instant scroll flag -- @usage scroll:scroll_to(vmath.vector3(0, 50, 0)) -- @usage scroll:scroll_to(vmath.vector3(0), true) function M.scroll_to(self, point, is_instant) @@ -342,16 +377,16 @@ function M.scroll_to(self, point, is_instant) cancel_animate(self) - self.animate = not is_instant + self.is_animate = not is_instant if is_instant then - self.target_pos = target - set_pos(self, target) + self.target_position = target + set_scroll_position(self, target) else gui.animate(self.content_node, gui.PROP_POSITION, target, gui.EASING_OUTSINE, self.style.ANIM_SPEED, 0, function() - self.animate = false - self.target_pos = target - set_pos(self, target) + self.is_animate = false + self.target_position = target + set_scroll_position(self, target) end) end @@ -359,11 +394,15 @@ function M.scroll_to(self, point, is_instant) end ---- Scroll to item in scroll by point index +--- Scroll to item in scroll by point index. -- @function scroll:scroll_to_index -- @tparam number index Point index -- @tparam[opt] bool skip_cb If true, skip the point callback function M.scroll_to_index(self, index, skip_cb) + if not self.points then + return + end + index = helper.clamp(index, 1, #self.points) if self.selected ~= index then @@ -378,7 +417,11 @@ function M.scroll_to_index(self, index, skip_cb) end - +--- Start scroll to target scroll percent +-- @function scroll:scroll_to_percent +-- @tparam point vector3 target percent +-- @tparam[opt] bool is_instant instant scroll flag +-- @usage scroll:scroll_to_percent(vmath.vector3(0.5, 0, 0)) function M.scroll_to_percent(self, percent, is_instant) local border = self.available_pos @@ -392,26 +435,56 @@ function M.scroll_to_percent(self, percent, is_instant) end +--- Return current scroll progress status. +-- Values will be in [0..1] interval +-- @function scroll:get_percent +-- @treturn vector3 New vector with scroll progress values function M.get_percent(self) - local x_perc = 1 - inverse_lerp(self.available_pos.x, self.available_pos.z, self.current_pos.x) - local y_perc = inverse_lerp(self.available_pos.w, self.available_pos.y, self.current_pos.y) + 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) return vmath.vector3(x_perc, y_perc, 0) end +--- Set scroll content size. +-- It will change content gui node size +-- @function scroll:set_size +-- @tparam vector3 size The new size for content node +-- @treturn druid.scroll Self instance +function M.set_size(self, size) + gui.set_size(self.content_node, size) + update_size(self) + + return self +end + + --- Enable or disable scroll inert. -- If disabled, scroll through points (if exist) -- If no points, just simple drag without inertion -- @function scroll:set_inert -- @tparam bool state Inert scroll state +-- @treturn druid.scroll Self instance function M.set_inert(self, state) - self.is_inert = state - + self._is_inert = state return self end +--- Return if scroll have inertion. +-- @function scroll:is_inert +-- @treturn bool If scroll have inertion +function M.is_inert(self) + return self._is_inert +end + + +--- Set extra size for scroll stretching. +-- Set 0 to disable stretching effect +-- @function scroll:set_extra_strech_size +-- @tparam number stretch_size Size in pixels of additional scroll area +-- @treturn druid.scroll Self instance function M.set_extra_strech_size(self, stretch_size) self.extra_stretch_size = stretch_size or self.style.EXTRA_STRECH_SIZE update_size(self) @@ -420,10 +493,19 @@ function M.set_extra_strech_size(self, stretch_size) end +--- Return vector of scroll size with width and height. +-- @function scroll:get_scroll_size +-- @treturn vector3 Available scroll size +function M.get_scroll_size(self) + return self.available_size +end + + --- Set points of interest. -- Scroll will always centered on closer points -- @function scroll:set_points -- @tparam table points Array of vector3 points +-- @treturn druid.scroll Self instance function M.set_points(self, points) self.points = points diff --git a/druid/base/scroll_legacy.lua b/druid/base/scroll_legacy.lua deleted file mode 100644 index 56a50aa..0000000 --- a/druid/base/scroll_legacy.lua +++ /dev/null @@ -1,538 +0,0 @@ ---- Component to handle scroll content. --- Scroll consist from two nodes: scroll parent and scroll input --- Scroll input the user input zone, it's static --- Scroll parent the scroll moving part, it will change position. --- Setup initial scroll size by changing scroll parent size. If scroll parent --- size will be less than scroll_input size, no scroll is available. For scroll --- parent size should be more than input size --- @module druid.scroll - ---- Component events --- @table Events --- @tfield druid_event on_scroll On scroll move callback --- @tfield druid_event on_scroll_to On scroll_to function callback --- @tfield druid_event on_point_scroll On scroll_to_index function callback - ---- Component fields --- @table Fields --- @tfield node node Scroll parent node --- @tfield node input_zone Scroll input node --- @tfield vector3 zone_size Current scroll content size --- @tfield number soft_size Soft zone size from style table --- @tfield vector3 center_offset Distance from node to node's center --- @tfield bool is_inert Flag, if scroll now moving by inertion --- @tfield vector3 inert Current inert speed --- @tfield vector3 pos Current scroll posisition --- @tfield vector3 target Current scroll target position - ---- Component style params --- @table Style --- @tfield number FRICT_HOLD Multiplier for inertion, while touching --- @tfield number FRICT Multiplier for free inertion --- @tfield number INERT_THRESHOLD Scroll speed to stop inertion --- @tfield number INERT_SPEED Multiplier for inertion speed --- @tfield number DEADZONE Deadzone for start scrol in pixels --- @tfield number SOFT_ZONE_SIZE Size of outside zone in pixels (for scroll back moving) --- @tfield number BACK_SPEED Scroll back returning lerp speed --- @tfield number ANIM_SPEED Scroll gui.animation speed for scroll_to function - -local Event = require("druid.event") -local helper = require("druid.helper") -local const = require("druid.const") -local component = require("druid.component") - -local M = component.create("scroll", { const.ON_UPDATE, const.ON_INPUT_HIGH }) - - --- Global on all scrolls --- TODO: remove it -M.current_scroll = nil - - -local function get_border(node) - local pivot = gui.get_pivot(node) - local pivot_offset = helper.get_pivot_offset(pivot) - local size = vmath.mul_per_elem(gui.get_size(node), gui.get_scale(node)) - return vmath.vector4( - -size.x*(0.5 + pivot_offset.x), - size.y*(0.5 + pivot_offset.y), - size.x*(0.5 - pivot_offset.x), - -size.y*(0.5 - pivot_offset.y) - ) -end - - -local function update_border(self) - local input_border = get_border(self.input_zone) - local content_border = get_border(self.node) - - -- border.x - min content.x node pos - -- border.y - min content.y node pos - -- border.z - max content.x node pos - -- border.w - max content.y node pos - self.border = vmath.vector4( - input_border.x - content_border.x, - -input_border.w + content_border.w, - input_border.z - content_border.z, - -input_border.y + content_border.y - ) - self.can_x = (self.border.x ~= self.border.z) - self.can_y = (self.border.y ~= self.border.w) -end - - - -local function set_pos(self, pos) - if self.pos.x ~= pos.x or self.pos.y ~= pos.y then - self.pos.x = pos.x - self.pos.y = pos.y - gui.set_position(self.node, self.pos) - - self.on_scroll:trigger(self:get_context(), self.pos) - end -end - - ---- Return scroll, if it outside of scroll area --- Using the lerp with BACK_SPEED koef -local function check_soft_target(self) - local t = self.target - local b = self.border - - if t.y < b.y then - t.y = helper.step(t.y, b.y, math.abs(t.y - b.y) * self.style.BACK_SPEED) - end - if t.x > b.x then - t.x = helper.step(t.x, b.x, math.abs(t.x - b.x) * self.style.BACK_SPEED) - end - if t.y > b.w then - t.y = helper.step(t.y, b.w, math.abs(t.y - b.w) * self.style.BACK_SPEED) - end - if t.x < b.z then - t.x = helper.step(t.x, b.z, math.abs(t.x - b.z) * self.style.BACK_SPEED) - end -end - - ---- Free inert update function -local function update_hand_scroll(self, dt) - local inert = self.inert - local delta_x = self.target.x - self.pos.x - local delta_y = self.target.y - self.pos.y - - if helper.sign(delta_x) ~= helper.sign(inert.x) then - inert.x = 0 - end - if helper.sign(delta_y) ~= helper.sign(inert.y) then - inert.y = 0 - end - - inert.x = inert.x + delta_x - inert.y = inert.y + delta_y - - inert.x = math.abs(inert.x) * helper.sign(delta_x) - inert.y = math.abs(inert.y) * helper.sign(delta_y) - - inert.x = inert.x * self.style.FRICT_HOLD - inert.y = inert.y * self.style.FRICT_HOLD - - set_pos(self, self.target) -end - - -local function get_zone_center(self) - return self.pos + self.center_offset -end - - ---- Find closer point of interest --- if no inert, scroll to next point by scroll direction --- if inert, find next point by scroll director --- @local -local function check_points(self) - if not self.points then - return - end - - local inert = self.inert - if not self.is_inert then - if math.abs(inert.x) > self.style.DEADZONE then - self:scroll_to_index(self.selected - helper.sign(inert.x)) - return - end - if math.abs(inert.y) > self.style.DEADZONE then - self:scroll_to_index(self.selected + helper.sign(inert.y)) - return - end - end - - -- Find closest point and point by scroll direction - -- Scroll to one of them (by scroll direction in priority) - local temp_dist = math.huge - local temp_dist_on_inert = math.huge - local index = false - local index_on_inert = false - local pos = get_zone_center(self) - for i = 1, #self.points do - local p = self.points[i] - local dist = helper.distance(pos.x, pos.y, p.x, p.y) - local on_inert = true - -- If inert ~= 0, scroll only by move direction - if inert.x ~= 0 and helper.sign(inert.x) ~= helper.sign(p.x - pos.x) then - on_inert = false - end - if inert.y ~= 0 and helper.sign(inert.y) ~= helper.sign(p.y - pos.y) then - on_inert = false - end - - if dist < temp_dist then - index = i - temp_dist = dist - end - if on_inert and dist < temp_dist_on_inert then - index_on_inert = i - temp_dist_on_inert = dist - end - end - - self:scroll_to_index(index_on_inert or index) -end - - -local function check_threshold(self) - local inert = self.inert - if not self.is_inert or vmath.length(inert) < self.style.INERT_THRESHOLD then - check_points(self) - inert.x = 0 - inert.y = 0 - end -end - - -local function update_free_inert(self, dt) - local inert = self.inert - if inert.x ~= 0 or inert.y ~= 0 then - self.target.x = self.pos.x + (inert.x * dt * self.style.INERT_SPEED) - self.target.y = self.pos.y + (inert.y * dt * self.style.INERT_SPEED) - - inert.x = inert.x * self.style.FRICT - inert.y = inert.y * self.style.FRICT - - -- Stop, when low inert speed and go to points - check_threshold(self) - end - - check_soft_target(self) - set_pos(self, self.target) -end - - ---- Cancel animation on other animation or input touch -local function cancel_animate(self) - if self.animate then - self.target = gui.get_position(self.node) - self.pos.x = self.target.x - self.pos.y = self.target.y - gui.cancel_animation(self.node, gui.PROP_POSITION) - self.animate = false - end -end - - -local function add_delta(self, dx, dy) - local t = self.target - local b = self.border - local soft = self.soft_size - - -- TODO: Can we calc it more easier? - -- A lot of calculations for every side of border - - -- Handle soft zones - -- Percent - multiplier for delta. Less if outside of scroll zone - local x_perc = 1 - local y_perc = 1 - - if t.x > b.x and dx < 0 then - x_perc = (soft - (b.x - t.x)) / soft - end - if t.x < b.z and dx > 0 then - x_perc = (soft - (t.x - b.z)) / soft - end - -- If disabled scroll by x - if not self.can_x then - x_perc = 0 - end - - if t.y < b.y and dy < 0 then - y_perc = (soft - (b.y - t.y)) / soft - end - if t.y > b.w and dy > 0 then - y_perc = (soft - (t.y - b.w)) / soft - end - -- If disabled scroll by y - if not self.can_y then - y_perc = 0 - end - - -- Reset inert if outside of scroll zone - if x_perc ~= 1 then - self.inert.x = 0 - end - if y_perc ~= 1 then - self.inert.y = 0 - end - - t.x = t.x + dx * x_perc - t.y = t.y + dy * y_perc -end - - ---- Component init function --- @function scroll:init --- @tparam node scroll_parent Gui node where placed scroll content. This node will change position --- @tparam node input_zone Gui node where input is catched -function M.init(self, scroll_parent, input_zone) - self.style = self:get_style() - self.node = self:get_node(scroll_parent) - self.input_zone = self:get_node(input_zone) - - self.zone_size = gui.get_size(self.input_zone) - self.soft_size = self.style.SOFT_ZONE_SIZE - - -- Distance from node to node's center - local offset = helper.get_pivot_offset(gui.get_pivot(self.input_zone)) - self.center_offset = vmath.vector3(self.zone_size) - self.center_offset.x = self.center_offset.x * offset.x - self.center_offset.y = self.center_offset.y * offset.y - - self.is_inert = true - self.inert = vmath.vector3(0) - self.pos = gui.get_position(self.node) - self.target = vmath.vector3(self.pos) - - self.input = { - touch = false, - start_x = 0, - start_y = 0, - side = false, - } - - update_border(self) - - self.on_scroll = Event() - self.on_scroll_to = Event() - self.on_point_scroll = Event() -end - - -function M.update(self, dt) - if self.input.touch then - if M.current_scroll == self then - update_hand_scroll(self, dt) - end - else - update_free_inert(self, dt) - end -end - - -function M.on_input(self, action_id, action) - if action_id ~= const.ACTION_TOUCH then - return false - end - local inp = self.input - local inert = self.inert - local result = false - - if gui.pick_node(self.input_zone, action.x, action.y) then - if action.pressed then - inp.touch = true - inp.start_x = action.x - inp.start_y = action.y - inert.x = 0 - inert.y = 0 - self.target.x = self.pos.x - self.target.y = self.pos.y - else - local dist = helper.distance(action.x, action.y, inp.start_x, inp.start_y) - if not M.current_scroll and dist >= self.style.DEADZONE then - local dx = math.abs(inp.start_x - action.x) - local dy = math.abs(inp.start_y - action.y) - inp.side = (dx > dy) and const.SIDE.X or const.SIDE.Y - - -- Check scroll side if we can scroll - if (self.can_x and inp.side == const.SIDE.X or - self.can_y and inp.side == const.SIDE.Y) then - M.current_scroll = self - end - end - end - end - - if inp.touch and not action.pressed then - if M.current_scroll == self then - add_delta(self, action.dx, action.dy) - result = true - end - end - - if action.released then - inp.touch = false - inp.side = false - if M.current_scroll == self then - M.current_scroll = nil - result = true - end - - check_threshold(self) - end - - return result -end - - ---- Start scroll to target point --- @function scroll:scroll_to --- @tparam point vector3 target point --- @tparam[opt] bool is_instant instant scroll flag --- @usage scroll:scroll_to(vmath.vector3(0, 50, 0)) --- @usage scroll:scroll_to(vmath.vector3(0), true) -function M.scroll_to(self, point, is_instant) - local b = self.border - local target = vmath.vector3(point) - target.x = helper.clamp(point.x - self.center_offset.x, b.x, b.z) - target.y = helper.clamp(point.y - self.center_offset.y, b.y, b.w) - - cancel_animate(self) - - self.animate = not is_instant - - if is_instant then - self.target = target - set_pos(self, target) - else - gui.animate(self.node, gui.PROP_POSITION, target, gui.EASING_OUTSINE, self.style.ANIM_SPEED, 0, function() - self.animate = false - self.target = target - set_pos(self, target) - end) - end - - self.on_scroll_to:trigger(self:get_context(), target, is_instant) -end - - ---- Start scroll to target scroll percent --- @function scroll:scroll_to_percent --- @tparam point vector3 target percent --- @tparam[opt] bool is_instant instant scroll flag --- @usage scroll:scroll_to_percent(vmath.vector3(0.5, 0, 0)) -function M.scroll_to_percent(self, percent, is_instant) - local border = self.border - - local size_x = math.abs(border.z - border.x) - if size_x == 0 then - size_x = 1 - end - local size_y = math.abs(border.w - border.y) - if size_y == 0 then - size_y = 1 - end - - local pos = vmath.vector3( - -size_x * percent.x + border.x, - -size_y * percent.y + border.y, - 0) - M.scroll_to(self, pos, is_instant) -end - - ---- Scroll to item in scroll by point index --- @function scroll:init --- @tparam number index Point index --- @tparam[opt] bool skip_cb If true, skip the point callback -function M.scroll_to_index(self, index, skip_cb) - index = helper.clamp(index, 1, #self.points) - - if self.selected ~= index then - self.selected = index - - if not skip_cb then - self.on_point_scroll:trigger(self:get_context(), index, self.points[index]) - end - end - - self:scroll_to(self.points[index]) -end - - ---- Set points of interest. --- Scroll will always centered on closer points --- @function scroll:set_points --- @tparam table points Array of vector3 points -function M.set_points(self, points) - self.points = points - -- cause of parent move in other side by y - for i = 1, #self.points do - self.points[i].y = -self.points[i].y - end - - table.sort(self.points, function(a, b) - return a.x > b.x or a.y < b.y - end) - check_threshold(self) -end - - ---- Enable or disable scroll inert. --- If disabled, scroll through points (if exist) --- If no points, just simple drag without inertion --- @function scroll:set_inert --- @tparam bool state Inert scroll state -function M.set_inert(self, state) - self.is_inert = state -end - - ---- Set the callback on scrolling to point (if exist) --- @function scroll:on_point_move --- @tparam function callback Callback on scroll to point of interest -function M.on_point_move(self, callback) - self.on_point_scroll:subscribe(callback) -end - - ---- Set the scroll possibly area --- @function scroll:set_border --- @tparam vector3 border Size of scrolling area -function M.set_border(self, content_size) - gui.set_size(self.node, content_size) - update_border(self) -end - - ---- Return current scroll progress --- @function scroll:get_scroll_percent --- @treturn vector3 Scroll progress -function M.get_scroll_percent(self) - local border = self.border - local size_x = math.abs(border.z - border.x) - if size_x == 0 then - size_x = 1 - end - - local size_y = math.abs(border.w - border.y) - if size_y == 0 then - size_y = 1 - end - local pos = self.pos - - return vmath.vector3( - (border.x - pos.x) / size_x, - (border.y - pos.y) / size_y, - 0 - ) -end - - -return M diff --git a/druid/base/text.lua b/druid/base/text.lua index 5a668ee..a6c7f2e 100644 --- a/druid/base/text.lua +++ b/druid/base/text.lua @@ -196,7 +196,7 @@ end --- Return true, if text with line break -- @function text:is_multiline --- @treturn boolean Is text node with line break +-- @treturn bool Is text node with line break function M.is_multiline(self) return gui.get_line_break(self.node) end diff --git a/druid/druid.lua b/druid/druid.lua index eb10f44..c3b4533 100644 --- a/druid/druid.lua +++ b/druid/druid.lua @@ -73,11 +73,11 @@ function M.set_default_style(style) end ---- Set text function. +--- Set text function -- Druid locale component will call this function -- to get translated text. After set_text_funtion -- all existing locale component will be updated --- @function druid.set_text_function(callback) +-- @function druid.set_text_function -- @tparam function callback Get localized text function function M.set_text_function(callback) settings.get_text = callback or const.EMPTY_FUNCTION diff --git a/druid/styles/default/style.lua b/druid/styles/default/style.lua index 8c5b049..782fc48 100644 --- a/druid/styles/default/style.lua +++ b/druid/styles/default/style.lua @@ -53,14 +53,15 @@ M["scroll"] = { SCROLL_WHEEL_SPEED = 10, DEADZONE = 20, - FRICT = 0.94, -- mult for free inert - FRICT_HOLD = 0.75, -- mult. for inert, while touching + FRICT = 0.93, -- mult for free inert + FRICT_HOLD = 0.79, -- mult. for inert, while touching INERT_THRESHOLD = 2.5, -- speed to stop inertion - INERT_SPEED = 27, -- koef. of inert speed + INERT_SPEED = 30, -- koef. of inert speed BACK_SPEED = 0.35, -- Lerp speed of return to soft position EXTRA_STRECH_SIZE = 100, -- size of outside zone (back move) + SMALL_CONTENT_SCROLL = true, } diff --git a/example/gui/main/main.gui b/example/gui/main/main.gui index 9cf5062..de6f1e6 100644 --- a/example/gui/main/main.gui +++ b/example/gui/main/main.gui @@ -5925,7 +5925,7 @@ nodes { } nodes { position { - x: -300.0 + x: -287.0 y: 0.0 z: 0.0 w: 1.0 @@ -7174,6 +7174,69 @@ nodes { template_node_child: false size_mode: SIZE_MODE_MANUAL } +nodes { + position { + x: -197.0 + y: 223.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_TEXT + blend_mode: BLEND_MODE_ALPHA + text: "View" + font: "game" + id: "scroll_smaller_view_hint" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + outline { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + shadow { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + adjust_mode: ADJUST_MODE_FIT + line_break: false + parent: "scroll_smaller_view" + layer: "" + inherit_alpha: true + alpha: 1.0 + outline_alpha: 0.0 + shadow_alpha: 0.0 + template_node_child: false + text_leading: 1.0 + text_tracking: 0.0 +} nodes { position { x: 0.0 @@ -7264,7 +7327,7 @@ nodes { blend_mode: BLEND_MODE_ALPHA text: "Content" font: "game" - id: "text" + id: "scroll_smaller_content_hint" xanchor: XANCHOR_NONE yanchor: YANCHOR_NONE pivot: PIVOT_CENTER diff --git a/example/gui/main/main.gui_script b/example/gui/main/main.gui_script index 7c39141..3cf9b6a 100644 --- a/example/gui/main/main.gui_script +++ b/example/gui/main/main.gui_script @@ -31,6 +31,7 @@ end local function init_top_panel(self) + self.druid:new_blocker("panel_top") self.druid:new_button("button_left/button", on_control_button, -1) self.druid:new_button("button_right/button", on_control_button, 1) self.header = self.druid:new_lang_text("text_header", "main_page") @@ -60,9 +61,9 @@ function init(self) window.set_listener(on_window_callback) - init_top_panel(self) init_swipe_control(self) - self.page = 4 + + self.page = 1 main_page.setup_page(self) text_page.setup_page(self) button_page.setup_page(self) @@ -70,6 +71,8 @@ function init(self) slider_page.setup_page(self) input_page.setup_page(self) + init_top_panel(self) + -- Refresh state on_control_button(self, 0) end diff --git a/example/page/scroll_page.lua b/example/page/scroll_page.lua index e49d018..f48ec74 100644 --- a/example/page/scroll_page.lua +++ b/example/page/scroll_page.lua @@ -24,7 +24,7 @@ local function init_scroll_with_grid(self) grid_scroll:set_size(grid:get_size()) - local scroll_slider = self.druid:new_slider("grid_scroll_pin", vmath.vector3(300, 0, 0), function(_, value) + local scroll_slider = self.druid:new_slider("grid_scroll_pin", vmath.vector3(287, 0, 0), function(_, value) grid_scroll:scroll_to_percent(vmath.vector3(value, 0, 0), true) end) @@ -55,6 +55,7 @@ function M.setup_page(self) -- Content with less size than view self.druid:new_scroll("scroll_smaller_view", "scroll_smaller_content") :set_extra_strech_size(0) + :set_inert(false) -- Scroll with points of interests self.druid:new_scroll("scroll_with_points", "scroll_with_points_content")