Adds ScrollableContainer, to be used with tall widgets (#8299)

And use it with KeyboardLayoutDialog.
reviewable/pr8323/r1
poire-z 3 years ago committed by GitHub
parent 8c29b71e45
commit ade89cb9b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1541,6 +1541,16 @@ function UIManager:widgetRepaint(widget, x, y)
if not widget then return end
logger.dbg("Explicit widgetRepaint:", widget.name or widget.id or tostring(widget), "@ (", x, ",", y, ")")
if widget.show_parent and widget.show_parent.cropping_widget then
-- The main widget parent of this subwidget has a cropping container: see if
-- this widget is a child of this cropping container
local cropping_widget = widget.show_parent.cropping_widget
if util.arrayReferences(cropping_widget, widget) then
-- Delegate the painting of this subwidget to its cropping widget container
cropping_widget:paintTo(Screen.bb, cropping_widget.dimen.x, cropping_widget.dimen.y)
return
end
end
widget:paintTo(Screen.bb, x, y)
end
@ -1558,6 +1568,19 @@ function UIManager:widgetInvert(widget, x, y, w, h)
if not widget then return end
logger.dbg("Explicit widgetInvert:", widget.name or widget.id or tostring(widget), "@ (", x, ",", y, ")")
if widget.show_parent and widget.show_parent.cropping_widget then
-- The main widget parent of this subwidget has a cropping container: see if
-- this widget is a child of this cropping container
local cropping_widget = widget.show_parent.cropping_widget
if util.arrayReferences(cropping_widget, widget) then
-- Invert only what intersects with the cropping container
local widget_region = Geom:new{x=x, y=y, w=w or widget.dimen.w, h=h or widget.dimen.h}
local crop_region = cropping_widget:getCropRegion()
local invert_region = crop_region:intersect(widget_region)
Screen.bb:invertRect(invert_region.x, invert_region.y, invert_region.w, invert_region.h)
return
end
end
Screen.bb:invertRect(x, y, w or widget.dimen.w, h or widget.dimen.h)
end

@ -144,6 +144,7 @@ function Button:init()
-- set FrameContainer content
self.frame = FrameContainer:new{
margin = self.margin,
show_parent = self.show_parent,
bordersize = self.bordersize,
background = self.background,
radius = self.radius,

@ -0,0 +1,449 @@
--[[--
ScrollableContainer allows scrolling its content (1 widget) within its own dimensions
This scrollable container needs to be known as widget.cropping_widget in
the widget using it that is passed to UIManager:show() for UIManager to
ensure proper interception of inner widget self-repainting/invert (mostly
used when flashing for UI feedback that we want to limit to the cropped
area).
If we notice some inner element flashing leaking outside the scrollable
area, it's probably some 'show_parent' forwarding missing from the main
widget to some of the inner widgets: chase the missing ones and add them.
--]]
local BD = require("ui/bidi")
local Blitbuffer = require("ffi/blitbuffer")
local Device = require("device")
local Geom = require("ui/geometry")
local GestureRange = require("ui/gesturerange")
local HorizontalScrollBar = require("ui/widget/horizontalscrollbar")
local InputContainer = require("ui/widget/container/inputcontainer")
local Math = require("optmath")
local UIManager = require("ui/uimanager")
local VerticalScrollBar = require("ui/widget/verticalscrollbar")
local Screen = Device.screen
local logger = require("logger")
local ScrollableContainer = InputContainer:new{
-- Events to ignore (ie: ignore_events={"hold", "hold_release"})
ignore_events = nil,
scroll_bar_width = Screen:scaleBySize(6),
-- Set to true if child widget is larger, false otherwise
_is_scrollable = nil,
-- Current scroll offset (use getScrolledOffset()/setScrolledOffset() to access them)
_scroll_offset_x = 0,
_scroll_offset_y = 0,
_max_scroll_offset_x = 0,
_max_scroll_offset_y = 0,
-- Internal state between events
_touch_pre_pan_was_inside = false,
_scrolling = false,
_scroll_relative_x = nil,
_scroll_relative_y = nil,
-- Scrollbar widgets, created as needed
_v_scroll_bar = nil,
_h_scroll_bar = nil,
-- Scratch buffer
_bb = nil,
_crop_w = nil,
_crop_h = nil,
_crop_dx = 0,
_mirroredUI = BD.mirroredUILayout(),
}
function ScrollableContainer:getScrollbarWidth(scroll_bar_width)
-- Return the width taken by the (default) scroll bar and its paddings
if not scroll_bar_width then
scroll_bar_width = self.scroll_bar_width
end
return 3 * scroll_bar_width
end
function ScrollableContainer:init()
if Device:isTouchDevice() then
local range = Geom:new{
x = 0, y = 0,
w = Screen:getWidth(),
h = Screen:getHeight(),
}
-- Unflatten self.ignore_events to table keys for cleaner code below
local ignore = {}
if self.ignore_events then
for _, evname in pairs(self.ignore_events) do
ignore[evname] = true
end
end
-- The following gestures need to be supported, depending on the
-- ways a user can move/scroll things:
-- Hold happens if he holds at start
-- Pan happens if he doesn't hold at start, but holds at end
-- Swipe happens if he doesn't hold at any moment
-- (Touch is needed for accurate pan)
self.ges_events = {}
self.ges_events.ScrollableTouch = not ignore.touch and { GestureRange:new{ ges = "touch", range = range } } or nil
self.ges_events.ScrollableSwipe = not ignore.swipe and { GestureRange:new{ ges = "swipe", range = range } } or nil
self.ges_events.ScrollableHold = not ignore.hold and { GestureRange:new{ ges = "hold", range = range } } or nil
self.ges_events.ScrollableHoldPan = not ignore.hold_pan and { GestureRange:new{ ges = "hold_pan", range = range } } or nil
self.ges_events.ScrollableHoldRelease = not ignore.hold_release and { GestureRange:new{ ges = "hold_release", range = range } } or nil
self.ges_events.ScrollablePan = not ignore.pan and { GestureRange:new{ ges = "pan", range = range } } or nil
self.ges_events.ScrollablePanRelease = not ignore.pan_release and { GestureRange:new{ ges = "pan_release", range = range } } or nil
end
end
function ScrollableContainer:initState()
local content_size = self[1]:getSize()
self._max_scroll_offset_x = math.max(0, content_size.w - self.dimen.w)
self._max_scroll_offset_y = math.max(0, content_size.h - self.dimen.h)
if self._max_scroll_offset_x == 0 and self._max_scroll_offset_y == 0 then
-- Inner widget fits entirely: no need for anything scrollable
self._is_scrollable = false
else
self._is_scrollable = true
self._crop_w = self.dimen.w
self._crop_h = self.dimen.h
if self._max_scroll_offset_y > 0 then
-- Adding a vertical scrollbar reduces the available width: recompute
self._max_scroll_offset_x = math.max(0, content_size.w - (self.dimen.w - 3*self.scroll_bar_width))
end
if self._max_scroll_offset_x > 0 then
-- Adding a horizontal scrollbar reduces the available height: recompute
self._max_scroll_offset_y = math.max(0, content_size.h - (self.dimen.h - 3*self.scroll_bar_width))
if self._max_scroll_offset_y > 0 then
-- And re-compute again if we have to now add a vertical scrollbar
self._max_scroll_offset_x = math.max(0, content_size.w - (self.dimen.w - 3*self.scroll_bar_width))
end
end
-- Scrollbars won't be classic sub-widgets, we'll handle their painting ourselves
if self._max_scroll_offset_y > 0 then
self._v_scroll_bar = VerticalScrollBar:new{
width = self.scroll_bar_width,
height = self.dimen.h,
scroll_callback = function(ratio)
self:scrollToRatio(nil, ratio)
end
}
self._crop_w = self.dimen.w - 3*self.scroll_bar_width
end
if self._max_scroll_offset_x > 0 then
self._h_scroll_bar_shift = 0
if self._v_scroll_bar then
-- Reduce its width so to not overlap with the vertical scroll bar
self._h_scroll_bar_shift = 3*self.scroll_bar_width
end
self._h_scroll_bar = HorizontalScrollBar:new{
height = self.scroll_bar_width,
width = self.dimen.w - self._h_scroll_bar_shift,
scroll_callback = function(ratio)
self:scrollToRatio(ratio, nil)
end
}
self._crop_h = self.dimen.h - 3*self.scroll_bar_width
end
if self._mirroredUI then
if self._v_scroll_bar then
self._crop_dx = self.dimen.w - self._crop_w
end
end
self:_updateScrollBars()
end
end
function ScrollableContainer:getCropRegion()
return Geom:new{
x = self.dimen.x + self._crop_dx,
y = self.dimen.y,
w = self._crop_w,
h = self._crop_h,
}
end
function ScrollableContainer:_updateScrollBars()
if self._v_scroll_bar then
local dheight = self._crop_h / (self._max_scroll_offset_y + self._crop_h)
local low = self._scroll_offset_y / (self._max_scroll_offset_y + self._crop_h)
local high = low + dheight
self._v_scroll_bar:set(low, high)
end
if self._h_scroll_bar then
local dwidth = self._crop_w / (self._max_scroll_offset_x + self._crop_w)
local low = self._scroll_offset_x / (self._max_scroll_offset_x + self._crop_w)
local high = low + dwidth
self._h_scroll_bar:set(low, high)
end
end
function ScrollableContainer:scrollToRatio(ratio_x, ratio_y)
if ratio_y then
local dy = ratio_y * (self._max_scroll_offset_y + self._crop_h)
self._scroll_offset_y = dy - Math.round(self._crop_h/2)
if self._scroll_offset_y < 0 then
self._scroll_offset_y = 0
end
if self._scroll_offset_y > self._max_scroll_offset_y then
self._scroll_offset_y = self._max_scroll_offset_y
end
end
if ratio_x then
local dx = ratio_x * (self._max_scroll_offset_x + self._crop_w)
self._scroll_offset_x = dx - Math.round(self._crop_w/2)
if self._scroll_offset_x < 0 then
self._scroll_offset_x = 0
end
if self._scroll_offset_x > self._max_scroll_offset_x then
self._scroll_offset_x = self._max_scroll_offset_x
end
end
self:_scrollBy(0, 0) -- get the additional work done
end
function ScrollableContainer:_scrollBy(dx, dy)
if self._mirroredUI then
dx = -dx
end
self._scroll_offset_x = self._scroll_offset_x + Math.round(dx)
self._scroll_offset_y = self._scroll_offset_y + Math.round(dy)
if self._scroll_offset_x < 0 then
self._scroll_offset_x = 0
end
if self._scroll_offset_y < 0 then
self._scroll_offset_y = 0
end
if self._scroll_offset_x > self._max_scroll_offset_x then
self._scroll_offset_x = self._max_scroll_offset_x
end
if self._scroll_offset_y > self._max_scroll_offset_y then
self._scroll_offset_y = self._max_scroll_offset_y
end
self:_updateScrollBars()
UIManager:setDirty(self.show_parent, function()
return "ui", self.dimen
end)
end
function ScrollableContainer:getScrolledOffset()
return Geom:new{
x = self._scroll_offset_x,
y = self._scroll_offset_y,
}
end
function ScrollableContainer:setScrolledOffset(offset_point)
if offset_point and offset_point.x and offset_point.y then
self._scroll_offset_x = offset_point.x
self._scroll_offset_y = offset_point.y
end
end
function ScrollableContainer:onCloseWidget()
if self._bb then
self._bb:free()
self._bb = nil
end
end
function ScrollableContainer:paintTo(bb, x, y)
if self[1] == nil then
return
end
self.dimen.x = x
self.dimen.y = y
if self._is_scrollable == nil then -- not checked yet
self:initState()
end
if not self._is_scrollable then
-- nothing to scroll: pass-through
self[1]:paintTo(bb, x, y)
return
end
local screen_size = Screen:getSize()
-- Create/Recreate the compose cache if we changed screen geometry
if not self._bb or self._bb:getWidth() ~= screen_size.w or self._bb:getHeight() ~= screen_size.h then
if self._bb then
self._bb:free()
end
-- create a canvas for our child widget to paint to
self._bb = Blitbuffer.new(screen_size.w, screen_size.h, bb:getType())
end
-- We need to fill it with our usual background color on each drawing,
-- to erase bits that may not be overwritten after a scroll
self._bb:fill(Blitbuffer.COLOR_WHITE)
local dx
if self._mirroredUI then
dx = self._max_scroll_offset_x - self._scroll_offset_x - self._crop_dx
else
dx = self._scroll_offset_x
end
self[1]:paintTo(self._bb, x - dx, y - self._scroll_offset_y)
bb:blitFrom(self._bb, x + self._crop_dx, y, x + self._crop_dx, y, self._crop_w, self._crop_h)
-- Draw our scrollbars over
if self._h_scroll_bar then
if self._mirroredUI then
self._h_scroll_bar:paintTo(bb, x + self._h_scroll_bar_shift, y + self.dimen.h - 2*self.scroll_bar_width)
else
self._h_scroll_bar:paintTo(bb, x, y + self.dimen.h - 2*self.scroll_bar_width)
end
end
if self._v_scroll_bar then
if self._mirroredUI then
self._v_scroll_bar:paintTo(bb, x + self.scroll_bar_width, y)
else
self._v_scroll_bar:paintTo(bb, x + self.dimen.w - 2*self.scroll_bar_width, y)
end
end
end
function ScrollableContainer:propagateEvent(event)
-- Override WidgetContainer:propagateEvent() (which propagates an event
-- to children before having it handled by the widget itself)
if not self._is_scrollable then
-- pass-through
return InputContainer.propagateEvent(self, event)
end
if event.handler == "onGesture" and event.argc == 1 then
local ges = event.args[1]
-- Don't propagate events that happen out of view (in the hidden
-- scrolled-out area) to child
if ges.pos and not ges.pos:intersectWith(self.dimen) then
return false -- we may handle it here
end
end
-- Give any event first to our scrollbars
if self._v_scroll_bar and self._v_scroll_bar:handleEvent(event) then
return true
end
if self._h_scroll_bar and self._h_scroll_bar:handleEvent(event) then
return true
end
-- Pass non-gestures events, and gestures event in the view, to our child
return InputContainer.propagateEvent(self, event)
end
function ScrollableContainer:onScrollableSwipe(_, ges)
if not self._is_scrollable then
return false
end
logger.dbg("ScrollableContainer:onScrollableSwipe", ges)
if not ges.pos:intersectWith(self.dimen) then
-- with swipe, ges.pos is swipe's start position, which should
-- be on us to consider it
return false
end
self._scrolling = false -- could have been set by "pan" event received before "swipe"
local direction = ges.direction
local distance = ges.distance
local sq_distance = math.floor(math.sqrt(distance*distance/2))
if direction == "north" then self:_scrollBy(0, distance)
elseif direction == "south" then self:_scrollBy(0, -distance)
elseif direction == "east" then self:_scrollBy(-distance, 0)
elseif direction == "west" then self:_scrollBy(distance, 0)
elseif direction == "northeast" then self:_scrollBy(-sq_distance, sq_distance)
elseif direction == "northwest" then self:_scrollBy(sq_distance, sq_distance)
elseif direction == "southeast" then self:_scrollBy(-sq_distance, -sq_distance)
elseif direction == "southwest" then self:_scrollBy(sq_distance, -sq_distance)
end
return true
end
function ScrollableContainer:onScrollableTouch(_, ges)
if not self._is_scrollable then
return false
end
-- First "pan" event may already be outside of us, we need to
-- remember any "touch" event on us prior to "pan"
logger.dbg("ScrollableContainer:onScrollableTouch", ges)
if ges.pos:intersectWith(self.dimen) then
self._touch_pre_pan_was_inside = true
self._scroll_relative_x = ges.pos.x
self._scroll_relative_y = ges.pos.y
else
self._touch_pre_pan_was_inside = false
end
return false
end
function ScrollableContainer:onScrollableHold(_, ges)
if not self._is_scrollable then
return false
end
logger.dbg("ScrollableContainer:onScrollableHold", ges)
if ges.pos:intersectWith(self.dimen) then
self._scrolling = true -- start of pan
self._scroll_relative_x = ges.pos.x
self._scroll_relative_y = ges.pos.y
return true
end
return false
end
function ScrollableContainer:onScrollableHoldPan(_, ges)
if not self._is_scrollable then
return false
end
logger.dbg("ScrollableContainer:onScrollableHoldPan", ges)
-- we may sometimes not see the "hold" event
if ges.pos:intersectWith(self.dimen) or self._scrolling or self._touch_pre_pan_was_inside then
self._touch_pre_pan_was_inside = false -- reset it
self._scrolling = true
return true
end
return false
end
function ScrollableContainer:onScrollableHoldRelease(_, ges)
if not self._is_scrollable then
return false
end
logger.dbg("ScrollableContainer:onScrollableHoldRelease", ges)
if self._scrolling or self._touch_pre_pan_was_inside then
self._scrolling = false
if not self._scroll_relative_x or not self._scroll_relative_y then
-- no previous event gave us accurate scroll info, ignore it
return false
end
self._scroll_relative_x = ges.pos.x - self._scroll_relative_x
self._scroll_relative_y = ges.pos.y - self._scroll_relative_y
self:_scrollBy(-self._scroll_relative_x, -self._scroll_relative_y)
self._scroll_relative_x = nil
self._scroll_relative_y = nil
return true
end
return false
end
function ScrollableContainer:onScrollablePan(_, ges)
if not self._is_scrollable then
return false
end
logger.dbg("ScrollableContainer:onScrollablePan", ges)
if ges.pos:intersectWith(self.dimen) or self._scrolling or self._touch_pre_pan_was_inside then
self._touch_pre_pan_was_inside = false -- reset it
self._scrolling = true
self._scroll_relative_x = ges.relative.x
self._scroll_relative_y = ges.relative.y
return true
end
return false
end
function ScrollableContainer:onScrollablePanRelease(_, ges)
if not self._is_scrollable then
return false
end
logger.dbg("ScrollableContainer:onScrollablePanRelease", ges)
if self._scrolling then
self:_scrollBy(-self._scroll_relative_x, -self._scroll_relative_y)
self._scrolling = false
self._scroll_relative_x = nil
self._scroll_relative_y = nil
return true
end
return false
end
return ScrollableContainer

@ -0,0 +1,128 @@
local BD = require("ui/bidi")
local Blitbuffer = require("ffi/blitbuffer")
local Device = require("device")
local Geom = require("ui/geometry")
local GestureRange = require("ui/gesturerange")
local InputContainer = require("ui/widget/container/inputcontainer")
local Size = require("ui/size")
local Screen = require("device").screen
local HorizontalScrollBar = InputContainer:new{
enable = true,
low = 0,
high = 1,
height = Size.padding.default,
width = Size.item.height_large, -- as in VerticalScrollBar
bordersize = Size.border.thin,
bordercolor = Blitbuffer.COLOR_BLACK,
radius = 0,
rectcolor = Blitbuffer.COLOR_BLACK,
-- minimal width of the thumb/knob/grip (usually showing the current
-- view size and position relative to the whole scrollable width):
min_thumb_size = Size.line.thick,
scroll_callback = nil,
-- extra touchable height (for scrolling with pan) can be larger than
-- the provided height (this is added on each side)
extra_touch_on_side_heightratio = 1, -- make it 3 x height
_mirroredUI = BD.mirroredUILayout(),
}
function HorizontalScrollBar:init()
self.extra_touch_on_side = math.ceil( self.extra_touch_on_side_heightratio * self.height)
if Device:isTouchDevice() then
local pan_rate = Screen.low_pan_rate and 2.0 or 5.0
self.ges_events = {
TapScroll = {
GestureRange:new{
ges = "tap",
range = function() return self.touch_dimen end,
},
},
HoldScroll = {
GestureRange:new{
ges = "hold",
range = function() return self.touch_dimen end,
},
},
HoldPanScroll = {
GestureRange:new{
ges = "hold_pan",
rate = pan_rate,
range = function() return self.touch_dimen end,
},
},
HoldReleaseScroll = {
GestureRange:new{
ges = "hold_release",
range = function() return self.touch_dimen end,
},
},
PanScroll = {
GestureRange:new{
ges = "pan",
rate = pan_rate,
range = function() return self.touch_dimen end,
},
},
PanScrollRelease = {
GestureRange:new{
ges = "pan_release",
range = function() return self.touch_dimen end,
},
}
}
end
end
function HorizontalScrollBar:onTapScroll(arg, ges)
if self.scroll_callback then
local ratio = (ges.pos.x - self.touch_dimen.x) / self.width
if self._mirroredUI then
ratio = 1 - ratio
end
self.scroll_callback(ratio)
return true
end
end
HorizontalScrollBar.onHoldScroll = HorizontalScrollBar.onTapScroll
HorizontalScrollBar.onHoldPanScroll = HorizontalScrollBar.onTapScroll
HorizontalScrollBar.onHoldReleaseScroll = HorizontalScrollBar.onTapScroll
HorizontalScrollBar.onPanScroll = HorizontalScrollBar.onTapScroll
HorizontalScrollBar.onPanScrollRelease = HorizontalScrollBar.onTapScroll
function HorizontalScrollBar:getSize()
return Geom:new{
w = self.width,
h = self.height
}
end
function HorizontalScrollBar:set(low, high)
self.low = low > 0 and low or 0
self.high = high < 1 and high or 1
end
function HorizontalScrollBar:paintTo(bb, x, y)
if not self.enable then return end
self.touch_dimen = Geom:new{
x = x,
y = y - self.extra_touch_on_side,
w = self.width,
h = self.height + 2 * self.extra_touch_on_side,
}
bb:paintBorder(x, y, self.width, self.height,
self.bordersize, self.bordercolor, self.radius)
if self._mirroredUI then
bb:paintRect(x + self.bordersize + (1-self.high) * self.width, y + self.bordersize,
math.max((self.width - 2 * self.bordersize) * (self.high - self.low), self.min_thumb_size),
self.height - 2 * self.bordersize,
self.rectcolor)
else
bb:paintRect(x + self.bordersize + self.low * self.width, y + self.bordersize,
math.max((self.width - 2 * self.bordersize) * (self.high - self.low), self.min_thumb_size),
self.height - 2 * self.bordersize,
self.rectcolor)
end
end
return HorizontalScrollBar

@ -14,6 +14,7 @@ local Language = require("ui/language")
local LineWidget = require("ui/widget/linewidget")
local MovableContainer = require("ui/widget/container/movablecontainer")
local RadioButtonTable = require("ui/widget/radiobuttontable")
local ScrollableContainer = require("ui/widget/container/scrollablecontainer")
local Size = require("ui/size")
local TextWidget = require("ui/widget/textwidget")
local UIManager = require("ui/uimanager")
@ -100,12 +101,15 @@ function KeyboardLayoutDialog:init()
},
})
-- (RadioButtonTable's width and padding setup is a bit fishy: we get
-- this to look ok by using a CenterContainer to ensure some padding)
local scroll_container_inner_width = self.title_bar:getSize().w - ScrollableContainer:getScrollbarWidth()
self.radio_button_table = RadioButtonTable:new{
radio_buttons = radio_buttons,
width = math.floor(self.width * 0.9),
width = scroll_container_inner_width - 2*Size.padding.large,
focused = true,
scroll = false,
parent = self,
show_parent = self,
face = self.face,
}
@ -119,6 +123,29 @@ function KeyboardLayoutDialog:init()
show_parent = self,
}
local max_radio_button_container_height = math.floor(Screen:getHeight()*0.9
- self.title_widget:getSize().h - self.title_bar:getSize().h
- Size.span.vertical_large*4 - self.button_table:getSize().h)
local radio_button_container_height = math.min(self.radio_button_table:getSize().h, max_radio_button_container_height)
-- Our scrollable container needs to be known as widget.cropping_widget in
-- the widget that is passed to UIManager:show() for UIManager to ensure
-- proper interception of inner widget self repainting/invert (mostly used
-- when flashing for UI feedback that we want to limit to the cropped area).
self.cropping_widget = ScrollableContainer:new{
dimen = Geom:new{
w = self.title_bar:getSize().w,
h = radio_button_container_height,
},
show_parent = self,
CenterContainer:new{
dimen = Geom:new{
w = scroll_container_inner_width,
h = self.radio_button_table:getSize().h,
},
self.radio_button_table,
},
}
self.dialog_frame = FrameContainer:new{
radius = Size.radius.window,
bordersize = Size.border.window,
@ -132,13 +159,7 @@ function KeyboardLayoutDialog:init()
VerticalSpan:new{
width = Size.span.vertical_large*2,
},
CenterContainer:new{
dimen = Geom:new{
w = self.title_bar:getSize().w,
h = self.radio_button_table:getSize().h,
},
self.radio_button_table,
},
self.cropping_widget, -- our ScrollableContainer
VerticalSpan:new{
width = Size.span.vertical_large*2,
},

@ -87,6 +87,7 @@ function RadioButton:update()
background = self.background,
radius = self.radius,
padding = self.padding,
show_parent = self.show_parent,
LeftContainer:new{
dimen = Geom:new{
w = self.width,

Loading…
Cancel
Save