mirror of
https://github.com/airstruck/luigi.git
synced 2025-11-18 12:25:06 +00:00
831 lines
21 KiB
Lua
831 lines
21 KiB
Lua
--[[--
|
|
Widget class.
|
|
|
|
@classmod Widget
|
|
--]]--
|
|
|
|
local STRICT = false
|
|
local ROOT = (...):gsub('[^.]*$', '')
|
|
|
|
local Backend = require(ROOT .. 'backend')
|
|
local Event = require(ROOT .. 'event')
|
|
local Attribute = require(ROOT .. 'attribute')
|
|
local Painter = require(ROOT .. 'painter')
|
|
local Font = Backend.Font
|
|
|
|
local Widget = {}
|
|
|
|
Event.injectBinders(Widget)
|
|
|
|
--[[--
|
|
API Properties
|
|
|
|
These properties may be useful when creating user interfaces,
|
|
and are a formal part of the API.
|
|
|
|
@section api
|
|
--]]--
|
|
|
|
--[[--
|
|
Whether this widget has keyboard focus.
|
|
|
|
Can be used by styles and themes. This value is automatically set by
|
|
the `Input` class, and should generally be treated as read-only.
|
|
--]]--
|
|
Widget.focused = false
|
|
|
|
--[[--
|
|
Whether the pointer is within this widget.
|
|
|
|
Can be used by styles and themes. This value is automatically set by
|
|
the `Input` class, and should generally be treated as read-only.
|
|
--]]--
|
|
Widget.hovered = false
|
|
|
|
--[[--
|
|
Table of mouse buttons pressed on this widget and not yet released,
|
|
keyed by mouse button name with booleans as values.
|
|
|
|
Can be used by styles and themes. Values are automatically set by
|
|
the `Input` class, and should generally be treated as read-only.
|
|
--]]--
|
|
Widget.pressed = nil
|
|
|
|
--[[--
|
|
Internal Properties
|
|
|
|
These properties are used internally, but are not likely to be useful
|
|
when creating user interfaces; they are not a formal part of the API
|
|
and may change at any time.
|
|
|
|
@section internal
|
|
--]]--
|
|
|
|
--[[--
|
|
Identifies this object as a widget.
|
|
|
|
Can be used to determine whether an unknown object is a widget.
|
|
--]]--
|
|
Widget.isWidget = true
|
|
|
|
--[[--
|
|
Whether the widget is currently being reshaped.
|
|
|
|
Used internally by `reshape` to prevent stack overflows when handling
|
|
`Reshape` events.
|
|
--]]--
|
|
Widget.isReshaping = false
|
|
|
|
--[[--
|
|
Whether this widget has a type.
|
|
|
|
Used by the @{attribute.type|type} attribute to determine whether to
|
|
run the type initializer when the widget's type is set. After a type
|
|
initializer has run, `hasType` becomes `true` and no other type
|
|
initializers should run on the widget.
|
|
--]]--
|
|
Widget.hasType = false
|
|
|
|
--[[--
|
|
The `Font` object associated with the widget.
|
|
--]]--
|
|
Widget.fontData = nil
|
|
|
|
--[[--
|
|
The `Text` object associated with the widget.
|
|
--]]--
|
|
Widget.textData = nil
|
|
|
|
|
|
--[[--
|
|
@section end
|
|
--]]--
|
|
|
|
Widget.typeDecorators = {
|
|
button = require(ROOT .. 'widget.button'),
|
|
check = require(ROOT .. 'widget.check'),
|
|
menu = require(ROOT .. 'widget.menu'),
|
|
['menu.item'] = require(ROOT .. 'widget.menu.item'),
|
|
progress = require(ROOT .. 'widget.progress'),
|
|
radio = require(ROOT .. 'widget.radio'),
|
|
sash = require(ROOT .. 'widget.sash'),
|
|
slider = require(ROOT .. 'widget.slider'),
|
|
status = require(ROOT .. 'widget.status'),
|
|
stepper = require(ROOT .. 'widget.stepper'),
|
|
text = require(ROOT .. 'widget.text'),
|
|
window = require(ROOT .. 'widget.window'),
|
|
}
|
|
|
|
--[[--
|
|
Static Functions
|
|
|
|
@section static
|
|
--]]--
|
|
|
|
--[[--
|
|
Register a custom widget type.
|
|
|
|
@static
|
|
|
|
@tparam string name
|
|
A unique name for this type of widget.
|
|
|
|
@tparam function(Widget) decorator
|
|
An initialization function for this type of widget.
|
|
--]]--
|
|
function Widget.register (name, decorator)
|
|
Widget.typeDecorators[name] = decorator
|
|
end
|
|
|
|
--[[--
|
|
@section end
|
|
--]]--
|
|
|
|
-- look for properties in attributes, Widget, style, and theme
|
|
local function metaIndex (self, property)
|
|
-- look in widget's own attributes
|
|
local A = self.attributeDescriptors[property] or Attribute[property]
|
|
if A then
|
|
local value = A.get and A.get(self, property)
|
|
or self.attributes[property]
|
|
if type(value) == 'function' then value = value(self) end
|
|
if value ~= nil then return value end
|
|
end
|
|
|
|
-- look in Widget class properties
|
|
local value = Widget[property]
|
|
if value ~= nil then return value end
|
|
|
|
-- look in style
|
|
local layout = self.layout
|
|
value = layout:getStyle():getProperty(self, property)
|
|
if value ~= nil then return value end
|
|
|
|
-- look in theme
|
|
return layout:getTheme():getProperty(self, property)
|
|
end
|
|
|
|
-- setting attributes triggers special behavior
|
|
local function metaNewIndex (self, property, value)
|
|
local A = self.attributeDescriptors[property] or Attribute[property]
|
|
if A then
|
|
if A.set then
|
|
A.set(self, value, property)
|
|
else
|
|
self.attributes[property] = value
|
|
end
|
|
else
|
|
if STRICT and Widget[property] == nil then
|
|
error(property .. ' is not a valid widget property.')
|
|
else
|
|
rawset(self, property, value)
|
|
end
|
|
end
|
|
end
|
|
|
|
local attributeNames = {}
|
|
|
|
for name in pairs(Attribute) do
|
|
if name ~= 'type' then -- type must be handled last
|
|
attributeNames[#attributeNames + 1] = name
|
|
end
|
|
end
|
|
|
|
attributeNames[#attributeNames + 1] = 'type'
|
|
|
|
--[[--
|
|
Widget pseudo-constructor.
|
|
|
|
@function Luigi.Widget
|
|
|
|
@within Constructor
|
|
|
|
@tparam Layout layout
|
|
The layout this widget belongs to.
|
|
|
|
@tparam[opt] table data
|
|
The data definition table for this widget.
|
|
This table is identical to the constructed widget.
|
|
|
|
@treturn Widget
|
|
A Widget instance.
|
|
--]]--
|
|
local function metaCall (Widget, layout, self)
|
|
self = self or {}
|
|
self.layout = layout
|
|
self.position = { x = nil, y = nil }
|
|
self.dimensions = { width = nil, height = nil }
|
|
self.attributes = {}
|
|
self.attributeDescriptors = {}
|
|
self.pressed = {}
|
|
self.painter = Painter(self)
|
|
|
|
setmetatable(self, { __index = metaIndex, __newindex = metaNewIndex })
|
|
|
|
for _, property in ipairs(attributeNames) do
|
|
local value = rawget(self, property)
|
|
rawset(self, property, nil)
|
|
self[property] = value
|
|
end
|
|
|
|
for k, v in ipairs(self) do
|
|
self[k] = v.isWidget and v or metaCall(Widget, self.layout, v)
|
|
self[k].parent = self
|
|
end
|
|
|
|
return self
|
|
end
|
|
|
|
function Widget:getMasterLayout ()
|
|
return self.layout.master or self.layout
|
|
end
|
|
|
|
--[[--
|
|
Define a custom attribute for this widget.
|
|
|
|
When an attribute is defined, the current value is stored locally and
|
|
removed from the widget's own properties and its attributes collection.
|
|
Then, the newly-defined setter is called with the stored value.
|
|
|
|
@tparam string name
|
|
The name of the attribute.
|
|
|
|
@tparam table descriptor
|
|
A table, optionally containing `get` and `set` functions (see `Attribute`).
|
|
|
|
@treturn Widget
|
|
Return this widget for chaining.
|
|
--]]--
|
|
function Widget:defineAttribute (name, descriptor)
|
|
local value = rawget(self, name)
|
|
if value == nil then value = self.attributes[name] end
|
|
self.attributeDescriptors[name] = descriptor or {}
|
|
rawset(self, name, nil)
|
|
self.attributes[name] = nil
|
|
self[name] = value
|
|
return self
|
|
end
|
|
|
|
--[[--
|
|
Fire an event on this widget and each ancestor.
|
|
|
|
If any event handler returns non-nil, stop the event from propagating.
|
|
|
|
@tparam string eventName
|
|
The name of the Event.
|
|
|
|
@tparam[opt] table data
|
|
Information about the event to send to handlers.
|
|
|
|
@treturn mixed
|
|
The first value returned by an event handler.
|
|
--]]--
|
|
function Widget:bubbleEvent (eventName, data)
|
|
local event = Event[eventName]
|
|
data = data or {}
|
|
data.target = self
|
|
for ancestor in self:eachAncestor(true) do
|
|
local result = event:emit(ancestor, data)
|
|
if result ~= nil then return result end
|
|
end
|
|
return event:emit(self.layout, data)
|
|
end
|
|
|
|
--[[--
|
|
Get widget's previous sibling.
|
|
|
|
@treturn Widget|nil
|
|
The widget's previous sibling, if any.
|
|
--]]--
|
|
function Widget:getPreviousSibling ()
|
|
local parent = self.parent
|
|
if not parent then return end
|
|
for i, widget in ipairs(parent) do
|
|
if widget == self then return parent[i - 1] end
|
|
end
|
|
end
|
|
|
|
--[[--
|
|
Get widget's next sibling.
|
|
|
|
@treturn Widget|nil
|
|
The widget's next sibling, if any.
|
|
--]]--
|
|
function Widget:getNextSibling ()
|
|
local parent = self.parent
|
|
if not parent then return end
|
|
for i, widget in ipairs(parent) do
|
|
if widget == self then return parent[i + 1] end
|
|
end
|
|
end
|
|
|
|
--[[--
|
|
Attempt to focus the widget.
|
|
|
|
Unfocus currently focused widget, and focus this widget if it's focusable.
|
|
|
|
@treturn boolean
|
|
true if this widget was focused, else false.
|
|
--]]--
|
|
function Widget:focus ()
|
|
local layout = self.layout
|
|
|
|
if layout.focusedWidget == self then
|
|
return true
|
|
end
|
|
|
|
if layout.focusedWidget then
|
|
layout.focusedWidget.focused = nil
|
|
Event.Blur:emit(self.layout, layout.focusedWidget)
|
|
layout.focusedWidget = nil
|
|
end
|
|
|
|
if self.focusable then
|
|
self.focused = true
|
|
layout.focusedWidget = self
|
|
Event.Focus:emit(self.layout, self)
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
--[[--
|
|
Get the next widget, depth-first.
|
|
|
|
If the widget has children, returns the first child.
|
|
Otherwise, returns the next sibling of the nearest possible ancestor.
|
|
Cycles back around to the layout root from the last widget in the tree.
|
|
|
|
@treturn Widget
|
|
The next widget in the tree.
|
|
--]]--
|
|
function Widget:getNextNeighbor ()
|
|
if #self > 0 then
|
|
return self[1]
|
|
end
|
|
for ancestor in self:eachAncestor(true) do
|
|
local nextWidget = ancestor:getNextSibling()
|
|
if nextWidget then return nextWidget end
|
|
end
|
|
return self.layout.root
|
|
end
|
|
|
|
-- get the last child of the last child of the last child of the...
|
|
local function getGreatestDescendant (widget)
|
|
while #widget > 0 do
|
|
widget = widget[#widget]
|
|
end
|
|
return widget
|
|
end
|
|
|
|
--[[--
|
|
Get the previous widget, depth-first.
|
|
|
|
Uses the reverse of the traversal order used by `getNextNeighbor`.
|
|
Cycles back around to the last widget in the tree from the layout root.
|
|
|
|
@treturn Widget
|
|
The previous widget in the tree.
|
|
--]]--
|
|
function Widget:getPreviousNeighbor ()
|
|
local layout = self.layout
|
|
|
|
if self == layout.root then
|
|
return getGreatestDescendant(self)
|
|
end
|
|
|
|
for ancestor in self:eachAncestor(true) do
|
|
local previousWidget = ancestor:getPreviousSibling()
|
|
if previousWidget then
|
|
return getGreatestDescendant(previousWidget)
|
|
end
|
|
if ancestor ~= self then return ancestor end
|
|
end
|
|
|
|
return layout.root
|
|
end
|
|
|
|
--[[--
|
|
Add a child to this widget.
|
|
|
|
@tparam Widget|table data
|
|
A widget or definition table representing a widget.
|
|
|
|
@treturn Widget
|
|
The newly added child widget.
|
|
--]]--
|
|
function Widget:addChild (data)
|
|
local layout = self.layout
|
|
local child = data and data.isWidget and data or Widget(layout, data or {})
|
|
|
|
self[#self + 1] = child
|
|
child.parent = self
|
|
child.layout = self.layout
|
|
|
|
return child
|
|
end
|
|
|
|
function Widget:calculateDimension (name)
|
|
-- If dimensions are already calculated, return them.
|
|
if self.dimensions[name] then
|
|
return self.dimensions[name]
|
|
end
|
|
|
|
-- Get minimum width/height from attributes.
|
|
local min = (name == 'width') and (self.minwidth or 0)
|
|
or (self.minheight or 0)
|
|
|
|
-- If width/height attribute is found (in widget, style or theme)
|
|
if self[name] then
|
|
-- and if width/height is "auto" then shrink to fit content
|
|
if self[name] == 'auto' then
|
|
self.dimensions[name] = self:calculateDimensionMinimum(name)
|
|
return self.dimensions[name]
|
|
end
|
|
-- else width/height should be a number; use that value,
|
|
-- clamped to minimum.
|
|
self.dimensions[name] = math.max(self[name], min)
|
|
return self.dimensions[name]
|
|
end
|
|
|
|
-- If the widget is a layout root (and has no width/height),
|
|
-- it's the same size as the window.
|
|
local parent = self.parent
|
|
if not parent then
|
|
local windowWidth, windowHeight = Backend.getWindowSize()
|
|
local size = name == 'width' and windowWidth or windowHeight
|
|
self.dimensions[name] = size
|
|
return self.dimensions[name]
|
|
end
|
|
|
|
-- Widgets expand to fit their parents when no width/height is specified.
|
|
local parentDimension = parent:calculateDimension(name)
|
|
parentDimension = parentDimension - (parent.margin or 0) * 2
|
|
parentDimension = parentDimension - (parent.padding or 0) * 2
|
|
|
|
-- If the dimension is in the opposite direction of the parent flow
|
|
-- (for example if parent.flow is 'x' and the dimension is 'height'),
|
|
-- then return the parent dimension.
|
|
local parentFlow = parent.flow or 'y'
|
|
if (parentFlow ~= 'x' and name == 'width')
|
|
or (parentFlow == 'x' and name == 'height') then
|
|
self.dimensions[name] = math.max(parentDimension, min)
|
|
return self.dimensions[name]
|
|
end
|
|
|
|
-- If the dimension is in the same direction as the parent flow
|
|
-- (for example if parent.flow is 'x' and the dimension is 'width'),
|
|
-- then return an equal portion of the unclaimed space in the parent.
|
|
local claimed = 0
|
|
local unsized = 1
|
|
for i, widget in ipairs(self.parent) do
|
|
if widget ~= self then
|
|
local value = widget[name]
|
|
if value == 'auto' then
|
|
if not widget.dimensions[name] then
|
|
widget.dimensions[name] = widget:calculateDimensionMinimum(name)
|
|
end
|
|
claimed = claimed + widget.dimensions[name]
|
|
elseif value then
|
|
local min = (name == 'width') and (widget.minwidth or 0)
|
|
or (widget.minheight or 0)
|
|
claimed = claimed + math.max(value, min)
|
|
else
|
|
unsized = unsized + 1
|
|
end
|
|
end
|
|
end
|
|
local size = (parentDimension - claimed) / unsized
|
|
|
|
size = math.max(size, min)
|
|
self.dimensions[name] = size
|
|
return size
|
|
end
|
|
|
|
function Widget:calculateRootPosition (axis)
|
|
local value = (axis == 'x' and self.left) or (axis ~= 'x' and self.top)
|
|
|
|
if value then
|
|
self.position[axis] = value
|
|
return value
|
|
end
|
|
|
|
local ww, wh = Backend.getWindowSize()
|
|
|
|
if axis == 'x' and type(self.width) == 'number' then
|
|
value = (ww - self.width) / 2
|
|
elseif axis ~= 'x' and type(self.height) == 'number' then
|
|
value = (wh - self.height) / 2
|
|
else
|
|
value = 0
|
|
end
|
|
|
|
self.position[axis] = value
|
|
return value
|
|
end
|
|
|
|
function Widget:calculatePosition (axis)
|
|
if self.position[axis] then
|
|
return self.position[axis]
|
|
end
|
|
local parent = self.parent
|
|
local scroll = 0
|
|
if not parent then
|
|
return self:calculateRootPosition(axis)
|
|
else
|
|
scroll = axis == 'x' and (parent.scrollX or 0)
|
|
or axis ~= 'x' and (parent.scrollY or 0)
|
|
end
|
|
local parentPos = parent:calculatePosition(axis)
|
|
local p = parentPos - scroll + (parent.margin or 0) + (parent.padding or 0)
|
|
local parentFlow = parent.flow or 'y'
|
|
for i, widget in ipairs(parent) do
|
|
if widget == self then
|
|
self.position[axis] = p
|
|
return p
|
|
end
|
|
if parentFlow == axis then
|
|
local dimension = (axis == 'x') and 'width' or 'height'
|
|
p = p + widget:calculateDimension(dimension)
|
|
end
|
|
end
|
|
self.position[axis] = 0
|
|
return 0
|
|
end
|
|
|
|
function Widget:calculateDimensionMinimum (name)
|
|
local dim = self[name]
|
|
local min = (name == 'width') and (self.minwidth or 0)
|
|
or (self.minheight or 0)
|
|
|
|
if type(dim) == 'number' then
|
|
return math.max(dim, min)
|
|
end
|
|
|
|
local value = 0
|
|
|
|
for _, child in ipairs(self) do
|
|
if (name == 'width' and self.flow == 'x')
|
|
or (name == 'height' and self.flow ~= 'x') then
|
|
value = value + child:calculateDimensionMinimum(name)
|
|
else
|
|
value = math.max(value, child:calculateDimensionMinimum(name))
|
|
end
|
|
end
|
|
|
|
if value > 0 then
|
|
local space = (self.margin or 0) * 2 + (self.padding or 0) * 2
|
|
value = value + space
|
|
end
|
|
|
|
return math.max(value, min)
|
|
end
|
|
|
|
--[[--
|
|
Get the widget's X coordinate.
|
|
|
|
@treturn number
|
|
The widget's X coordinate.
|
|
--]]--
|
|
function Widget:getX ()
|
|
return self:calculatePosition('x')
|
|
end
|
|
|
|
--[[--
|
|
Get the widget's Y coordinate.
|
|
|
|
@treturn number
|
|
The widget's Y coordinate.
|
|
--]]--
|
|
function Widget:getY ()
|
|
return self:calculatePosition('y')
|
|
end
|
|
|
|
--[[--
|
|
Get the widget's calculated width.
|
|
|
|
@treturn number
|
|
The widget's calculated width.
|
|
--]]--
|
|
function Widget:getWidth ()
|
|
return self:calculateDimension('width')
|
|
end
|
|
|
|
--[[--
|
|
Get the widget's calculated height.
|
|
|
|
@treturn number
|
|
The widget's calculated height.
|
|
--]]--
|
|
function Widget:getHeight ()
|
|
return self:calculateDimension('height')
|
|
end
|
|
|
|
--[[--
|
|
Get the content width.
|
|
|
|
Gets the combined width of the widget's children.
|
|
|
|
@treturn number
|
|
The content width.
|
|
--]]--
|
|
function Widget:getContentWidth ()
|
|
if not self.layout.isReady then return 0 end
|
|
local width = 0
|
|
if self.flow == 'x' then
|
|
for _, child in ipairs(self) do
|
|
width = width + child:getWidth()
|
|
end
|
|
else
|
|
for _, child in ipairs(self) do
|
|
width = math.max(width, child:getWidth())
|
|
end
|
|
end
|
|
return width
|
|
end
|
|
|
|
--[[--
|
|
Get the content height.
|
|
|
|
Gets the combined height of the widget's children.
|
|
|
|
@treturn number
|
|
The content height.
|
|
--]]--
|
|
function Widget:getContentHeight ()
|
|
if not self.layout.isReady then return 0 end
|
|
local height = 0
|
|
if self.flow ~= 'x' then
|
|
for _, child in ipairs(self) do
|
|
height = height + child:getHeight()
|
|
end
|
|
else
|
|
for _, child in ipairs(self) do
|
|
height = math.max(height, child:getHeight())
|
|
end
|
|
end
|
|
return height
|
|
end
|
|
|
|
function Widget:getFont ()
|
|
if not self.fontData then
|
|
self.fontData = Font(self.font, self.size)
|
|
end
|
|
return self.fontData
|
|
end
|
|
|
|
--[[--
|
|
Get x/y/width/height values describing a rectangle within the widget.
|
|
|
|
@tparam boolean useMargin
|
|
Whether to adjust the rectangle based on the widget's margin.
|
|
|
|
@tparam boolean usePadding
|
|
Whether to adjust the rectangle based on the widget's padding.
|
|
|
|
@treturn number
|
|
The upper left corner's X position.
|
|
|
|
@treturn number
|
|
The upper left corner's Y position.
|
|
|
|
@treturn number
|
|
The rectangle's width
|
|
|
|
@treturn number
|
|
The rectangle's height
|
|
--]]--
|
|
function Widget:getRectangle (useMargin, usePadding)
|
|
local x, y = self:getX(), self:getY()
|
|
local w, h = self:getWidth(), self:getHeight()
|
|
local function shrink(amount)
|
|
x = x + amount
|
|
y = y + amount
|
|
w = w - amount * 2
|
|
h = h - amount * 2
|
|
end
|
|
if useMargin then
|
|
shrink(self.margin or 0)
|
|
end
|
|
if usePadding then
|
|
shrink(self.padding or 0)
|
|
end
|
|
return math.floor(x), math.floor(y), math.floor(w), math.floor(h)
|
|
end
|
|
|
|
--[[--
|
|
Determine whether a point is within a widget.
|
|
|
|
@tparam number x
|
|
The point's X coordinate.
|
|
|
|
@tparam number y
|
|
The point's Y coordinate.
|
|
|
|
@treturn boolean
|
|
true if the point is within the widget, else false.
|
|
--]]--
|
|
function Widget:isAt (x, y)
|
|
local x1, y1, w, h = self:getRectangle()
|
|
local x2, y2 = x1 + w, y1 + h
|
|
return (x1 <= x) and (x2 >= x) and (y1 <= y) and (y2 >= y)
|
|
end
|
|
|
|
--[[--
|
|
Iterate widget's ancestors.
|
|
|
|
@tparam boolean includeSelf
|
|
Whether to include this widget as the first result.
|
|
|
|
@treturn function
|
|
Returns an iterator function that returns widgets.
|
|
|
|
@usage
|
|
for ancestor in myWidget:eachAncestor(true) do
|
|
print(widget.type or 'generic')
|
|
end
|
|
--]]--
|
|
function Widget:eachAncestor (includeSelf)
|
|
local instance = includeSelf and self or self.parent
|
|
return function()
|
|
local widget = instance
|
|
if not widget then return end
|
|
instance = widget.parent
|
|
return widget
|
|
end
|
|
end
|
|
|
|
function Widget:paint ()
|
|
return self.painter:paint()
|
|
end
|
|
|
|
--[[--
|
|
Reshape the widget.
|
|
|
|
Clears calculated widget dimensions, allowing them to be recalculated, and
|
|
fires a Reshape event (does not bubble). Called recursively for each child.
|
|
|
|
When setting a widget's width or height, this function is automatically called
|
|
on the parent widget.
|
|
--]]--
|
|
function Widget:reshape ()
|
|
if self.isReshaping then return end
|
|
self.isReshaping = true
|
|
|
|
self:scrollBy(0, 0)
|
|
|
|
self.position = {}
|
|
self.dimensions = {}
|
|
|
|
self.textData = nil
|
|
|
|
Event.Reshape:emit(self, { target = self })
|
|
for _, child in ipairs(self) do
|
|
if child.reshape then
|
|
child:reshape()
|
|
end
|
|
end
|
|
local items = self.items
|
|
if items then
|
|
for _, child in ipairs(items) do
|
|
if child.reshape then
|
|
child:reshape()
|
|
end
|
|
end
|
|
end
|
|
self.isReshaping = nil
|
|
end
|
|
|
|
function Widget:scrollBy (amount)
|
|
if not self.scroll then return end
|
|
--TODO: eliminate redundancy
|
|
if self.flow == 'x' then
|
|
if not self.scrollX then self.scrollX = 0 end
|
|
local scrollX = self.scrollX - amount * 10
|
|
local inner = math.max(self:getContentWidth(), self.innerWidth or 0)
|
|
local maxX = inner - self:getWidth()
|
|
+ (self.padding or 0) * 2 + (self.margin or 0) * 2
|
|
scrollX = math.max(math.min(scrollX, maxX), 0)
|
|
if scrollX ~= self.scrollX then
|
|
self.scrollX = scrollX
|
|
self:reshape()
|
|
return true
|
|
end
|
|
else
|
|
if not self.scrollY then self.scrollY = 0 end
|
|
local scrollY = self.scrollY - amount * 10
|
|
local inner = math.max(self:getContentHeight(), self.innerHeight or 0)
|
|
local maxY = inner - self:getHeight()
|
|
+ (self.padding or 0) * 2 + (self.margin or 0) * 2
|
|
scrollY = math.max(math.min(scrollY, maxY), 0)
|
|
if scrollY ~= self.scrollY then
|
|
self.scrollY = scrollY
|
|
self:reshape()
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
|
|
return setmetatable(Widget, { __call = metaCall })
|