Skip to content

Commit ce56156

Browse files
authored
no_overflow & SMODS.UIScrollBox: Cutting-edge technology (#1232)
* `no_overflow` property implementation & SMODS.UIScrollBox implementation * lsp is done I guess? * fix no_overflow size constrain bypass during filling * cache point in overflow boundaries check * Correct target function in patch comment * Add function to reload stencil stack * Cleanup values in stencil on reset * Use SMODS.merge_defaults; Move LSP; Ready for merge? * Bring back line break here * Nope, move it here
1 parent 48f1ab4 commit ce56156

File tree

3 files changed

+347
-0
lines changed

3 files changed

+347
-0
lines changed

lovely/ui_overflow.toml

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
[manifest]
2+
version = "1.0.0"
3+
dump_lua = true
4+
priority = -10
5+
6+
# Allow use stencil in global canvas
7+
# Game:draw()
8+
[[patches]]
9+
[patches.pattern]
10+
target = "game.lua"
11+
pattern = '''love.graphics.setCanvas{self.CANVAS}'''
12+
position = "at"
13+
payload = '''
14+
love.graphics.setCanvas{self.CANVAS, stencil = true}
15+
'''
16+
match_indent = true
17+
18+
# Limit overflow container size
19+
# UIBox:calculate_xywh()
20+
[[patches]]
21+
[patches.pattern]
22+
target = "engine/ui.lua"
23+
pattern = '''_nt.h = math.max(_ct.h + padding, _nt.h)-- '''
24+
position = "after"
25+
payload = '''
26+
if node.config and node.config.no_overflow then
27+
if node.config.w then
28+
_nt.w = node.config.w
29+
elseif node.config.maxw then
30+
_nt.w = math.min(_nt.w, node.config.maxw)
31+
end
32+
if node.config.h then
33+
_nt.h = node.config.h
34+
elseif node.config.maxh then
35+
_nt.h = math.min(_nt.h, node.config.maxh)
36+
end
37+
end
38+
'''
39+
match_indent = true
40+
41+
# Prevent text be rescaled by overflow container
42+
# UIBox:calculate_xywh()
43+
[[patches]]
44+
[patches.pattern]
45+
target = "engine/ui.lua"
46+
pattern = '''fac = fac*restriction/(node.config.maxw and _ct.w or _ct.h)'''
47+
position = "after"
48+
payload = '''
49+
if node.config.no_overflow then fac = _scale or 1 end
50+
'''
51+
match_indent = true
52+
53+
# Limit overflow container size during filling
54+
# UIElement:set_wh()
55+
[[patches]]
56+
[patches.pattern]
57+
target = "engine/ui.lua"
58+
pattern = '''if w.UIT == G.UIT.C then w.T.h = _max_h end'''
59+
position = "after"
60+
payload = '''
61+
if w.config and w.config.no_overflow then
62+
if w.config.w then
63+
w.T.w = w.config.w
64+
elseif w.config.maxw then
65+
w.T.w = math.min(w.T.w, w.config.maxw)
66+
end
67+
if w.config.h then
68+
w.T.h = w.config.h
69+
elseif w.config.maxh then
70+
w.T.h = math.min(w.T.h, w.config.maxh)
71+
end
72+
end
73+
'''
74+
match_indent = true
75+
76+
# Exclude overflowed elements from colliding
77+
# Controller:get_cursor_collision()
78+
[[patches]]
79+
[patches.pattern]
80+
target = "engine/controller.lua"
81+
pattern = '''if v:collides_with_point(cursor_trans) and not v.REMOVED then'''
82+
position = "at"
83+
payload = '''
84+
if v:collides_with_point(cursor_trans) and not v.REMOVED and v:inside_overflow_boundaries(cursor_trans) then
85+
'''
86+
match_indent = true
87+
overwrite = false

lsp_def/ui.lua

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
SMODS.GUI = {}
44
SMODS.GUI.DynamicUIManager = {}
55

6+
--- @type table<function>
7+
SMODS.stencil_stack = {}
8+
69
---@type string|"achievements"|"config"|"credits"|"mod_desc"|"additions"
710
SMODS.LAST_SELECTED_MOD_TAB = ""
811

@@ -53,14 +56,76 @@ G.UIT = {
5356
---@field vert? boolean Sets if the text is drawn vertically.
5457
---@field object? Node Object to render.
5558
---@field role? "Major"|"Minor"|"Glued" Sets object's role type.
59+
---@field no_overflow? boolean Renders node as overflow container: constrain it's size, truncate drawing and prevent colliding child nodes which go outside of parent's boundaries
5660

5761
--- Internal class for annotating UIBox/UIElement tables before being turned into objects.
5862
---@class UINode: table
5963
---@field n G.UIT Type of UIBox/UIElement
6064
---@field config UINode.config Config of the UINode.
6165
---@field nodes? UINode[] Child UINodes
6266

67+
--
68+
69+
---@class SMODS.UIScrollBox.input
70+
---@field content Moveable | { definition: UINode, config: table, T?: table } Moveable or UIBox definition to render inside scrollable content (passed to G.UIT.O).
71+
---@field container? { node_config?: UINode.config, config?: table, T?: table } UIBox args for scroll container which will be moved to create scroll effect.
72+
---@field overflow? { node_config?: UINode.config, config?: table, T?: table } UIBox args for main element.
73+
---@field progress? { x?: number, y?: number } Value of scroll content relative offset in directions (0-1). Keeps reference for original table.
74+
---@field offset? { x?: number, y?: number } Value of scroll content absolute offset in directions (in game units). Keeps reference for original table.
75+
---@field sync_mode? "offset" | "progress" | "none" Sync mode. `offset` sync progress to match offset, `progress` sync offset to match progress, `none` disables syncing. Default is `progress`.
76+
---@field scroll_move? fun(self: SMODS.UIScrollBox, dt: number) Function which called every frame before scroll syncing and can be used to perform automatic scrolling.
77+
78+
--- Element for displaying scrollable content
79+
---@class SMODS.UIScrollBox: UIBox
80+
---@field content Moveable Displayed content.
81+
---@field content_container UIBox Container which positions `content` according to scroll offset.
82+
---@field scroll_args SMODS.UIScrollBox.input Input args
83+
---@field scroll_progress { x: number, y: number } Relative offset of scroll content in directions (0-1). Keeps reference for original table.
84+
---@field scroll_offset { x: number, y: number } Absolute offset of scroll content in directions (in game units). Keeps reference for original table.
85+
---@field scroll_sync_mode "offset" | "progress" | "none" Sync mode. `offset` sync progress to match offset, `progress` sync offset to match progress, `none` disables syncing. Default is `progress`.
86+
---@overload fun(args: SMODS.UIScrollBox.input): SMODS.UIScrollBox
87+
SMODS.UIScrollBox = {}
88+
SMODS.UIScrollBox.__index = SMODS.UIScrollBox
89+
SMODS.UIScrollBox.super = UIBox
90+
91+
---@return number, number
92+
--- Distance of content overflow in both directions
93+
function SMODS.UIScrollBox:get_scroll_distance() end
94+
95+
--- Update offset to match progress. Called every frame if `scroll_sync_mode = "progress"`
96+
function SMODS.UIScrollBox:sync_scroll_offset() end
97+
98+
--- Update progress to match offset. Called every frame if `scroll_sync_mode = "offset"`
99+
function SMODS.UIScrollBox:sync_scroll_progress() end
100+
101+
---@param t? { x?: number, y?: number }
102+
--- Set new table for offset (keeps reference), and sync progress to match new offset
103+
function SMODS.UIScrollBox:set_scroll_offset(t) end
104+
105+
---@param t? { x?: number, y?: number }
106+
--- Set new table for progress (keeps reference), and sync offset to match new progress
107+
function SMODS.UIScrollBox:set_scroll_progress(t) end
108+
109+
---@param dt number
110+
---@param init? boolean Is sync called during initialization
111+
--- Perform syncing according to `scroll_sync_mode`, and position elements to match result offset
112+
function SMODS.UIScrollBox:sync_scroll(dt, init) end
113+
63114
-- UI Functions
115+
116+
---@param stencil_fn fun(exit?: boolean)
117+
--- Add new stencil to stencil stack; result stencil is sum of all stencils in stack
118+
function SMODS.push_to_stencil_stack(stencil_fn) end
119+
120+
--- Discard last applied stencil in stack
121+
function SMODS.pop_from_stencil_stack() end
122+
123+
--- Cleanup stencil stack
124+
function SMODS.reset_stencil_stack() end
125+
126+
--- Reload stencil stack by cleaning up current stencil and redrawing all stencils from stack
127+
function SMODS.reload_stencil_stack() end
128+
64129
---@param str string
65130
---@return any
66131
--- Unpacks provided string.

src/ui.lua

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,201 @@
11
SMODS.GUI = {}
22
SMODS.GUI.DynamicUIManager = {}
33

4+
-- used to properly truncate overflow content inside another overflow content
5+
SMODS.stencil_stack = {}
6+
7+
function SMODS.push_to_stencil_stack(stencil_fn)
8+
assert(type(stencil_fn) == "function", "No stencil function passed to SMODS.push_to_stencil_stack")
9+
local old_level = #SMODS.stencil_stack
10+
local new_level = old_level + 1
11+
12+
love.graphics.setStencilTest("equal", old_level)
13+
love.graphics.stencil(function()
14+
stencil_fn(false)
15+
end, "increment", 1, true)
16+
love.graphics.setStencilTest("equal", new_level)
17+
18+
SMODS.stencil_stack[new_level] = stencil_fn
19+
end
20+
function SMODS.pop_from_stencil_stack()
21+
local old_level = #SMODS.stencil_stack
22+
local new_level = old_level - 1
23+
24+
local stencil_fn = SMODS.stencil_stack[old_level]
25+
if not stencil_fn then
26+
return
27+
end
28+
29+
love.graphics.setStencilTest("equal", old_level)
30+
love.graphics.stencil(function()
31+
stencil_fn(true)
32+
end, "decrement", 1, true)
33+
love.graphics.setStencilTest("equal", new_level)
34+
35+
SMODS.stencil_stack[old_level] = nil
36+
end
37+
function SMODS.reset_stencil_stack()
38+
EMPTY(SMODS.stencil_stack)
39+
love.graphics.setStencilTest()
40+
love.graphics.stencil(function() end)
41+
end
42+
function SMODS.reload_stencil_stack()
43+
local stack_snapshot = SMODS.shallow_copy(SMODS.stencil_stack)
44+
SMODS.reset_stencil_stack()
45+
for _, stencil_fn in ipairs(stack_snapshot) do
46+
SMODS.push_to_stencil_stack(stencil_fn)
47+
end
48+
end
49+
50+
local gameDrawRef = Game.draw
51+
function Game:draw(...)
52+
SMODS.reset_stencil_stack()
53+
gameDrawRef(self, ...)
54+
end
55+
56+
--
57+
58+
local uieDrawChildrenRef = UIElement.draw_children
59+
function UIElement:draw_children(...)
60+
local stenciled = false
61+
if self.states.visible and self.config and self.config.no_overflow then
62+
-- draw stencil for overflow container
63+
stenciled = true
64+
SMODS.push_to_stencil_stack(function(exit)
65+
prep_draw(self, 1)
66+
love.graphics.scale(1 / G.TILESIZE)
67+
love.graphics.setColor(0, 0, 0, 1)
68+
69+
if self.config.r and self.VT.w > 0.01 then
70+
self:draw_pixellated_rect("fill", 0)
71+
else
72+
love.graphics.rectangle("fill", 0, 0, self.VT.w * G.TILESIZE, self.VT.h * G.TILESIZE)
73+
end
74+
75+
love.graphics.pop()
76+
end)
77+
end
78+
uieDrawChildrenRef(self, ...)
79+
-- cancel stencil for overflow container
80+
if stenciled then SMODS.pop_from_stencil_stack() end
81+
end
82+
83+
84+
-- collision check
85+
function Node:inside_overflow_boundaries(point)
86+
-- Use cached value if present for current point
87+
if self.overflow_check_timer == G.TIMERS.REAL and self.overflow_check_point == point then
88+
return self.overflow_check_result or false
89+
end
90+
self.overflow_check_timer = G.TIMERS.REAL
91+
self.overflow_check_point = point
92+
local r = true
93+
94+
-- No parent = no overflow can be done so collide as usual
95+
if not self.parent then r = true
96+
-- If parent has overflow then we should check do we collide with it and if not, all children in it cannot be collided too
97+
elseif self.parent.config and self.parent.config.no_overflow and not Node.collides_with_point(self.parent, point) then r = false
98+
-- Otherwise process all parents looking for first non-collideable overflow
99+
else r = Node.inside_overflow_boundaries(self.parent, point) end
100+
101+
self.overflow_check_result = r
102+
return r
103+
end
104+
105+
--
106+
107+
SMODS.UIScrollBox = UIBox:extend()
108+
function SMODS.UIScrollBox:init(args)
109+
args = SMODS.merge_defaults(args, { content = {}, container = {}, overflow = {}, sync_mode = "progress" })
110+
args.progress = SMODS.merge_defaults(args.progress, { x = 0, y = 0 })
111+
args.offset = SMODS.merge_defaults(args.offset, { x = 0, y = 0 })
112+
113+
self.scroll_args = args
114+
self.scroll_progress = args.progress
115+
self.scroll_offset = args.offset
116+
self.scroll_sync_mode = args.sync_mode
117+
118+
if args.content and args.content.is and args.content:is(Object) then
119+
self.content = args.content
120+
else
121+
self.content = UIBox(args.content)
122+
end
123+
124+
args.container.config = SMODS.merge_defaults(args.container.config, { align = "cm", offset = { x = 0, y = 0 } })
125+
args.container.node_config = SMODS.merge_defaults(args.container.node_config, { colour = G.C.CLEAR })
126+
args.container.definition = {
127+
n = G.UIT.ROOT,
128+
config = args.container.node_config,
129+
nodes = {
130+
{
131+
n = G.UIT.O,
132+
config = {
133+
object = self.content,
134+
},
135+
},
136+
},
137+
}
138+
self.content_container = UIBox(args.container)
139+
140+
args.overflow.config = SMODS.merge_defaults(args.overflow.config, {})
141+
args.overflow.node_config = SMODS.merge_defaults(args.overflow.node_config, { colour = G.C.CLEAR, no_overflow = true })
142+
args.overflow.definition = {
143+
n = G.UIT.ROOT,
144+
config = args.overflow.node_config,
145+
nodes = {
146+
{
147+
n = G.UIT.O,
148+
config = {
149+
object = self.content_container,
150+
},
151+
},
152+
},
153+
}
154+
155+
UIBox.init(self, args.overflow)
156+
157+
self:sync_scroll(0, true)
158+
end
159+
function SMODS.UIScrollBox:get_scroll_distance()
160+
return math.max(0, self.content_container.T.w - self.T.w), math.max(0, self.content_container.T.h - self.T.h)
161+
end
162+
function SMODS.UIScrollBox:sync_scroll_offset()
163+
local dx, dy = self:get_scroll_distance()
164+
self.scroll_offset.x = dx * (self.scroll_progress.x or 0)
165+
self.scroll_offset.y = dy * (self.scroll_progress.y or 0)
166+
end
167+
function SMODS.UIScrollBox:sync_scroll_progress()
168+
local dx, dy = self:get_scroll_distance()
169+
self.scroll_progress.x = (dx == 0 and 0) or ((self.offset.x or 0) / dx)
170+
self.scroll_progress.y = (dy == 0 and 0) or ((self.offset.y or 0) / dy)
171+
end
172+
function SMODS.UIScrollBox:set_scroll_offset(t)
173+
self.scroll_offset = SMODS.merge_defaults(t, { x = 0, y = 0 })
174+
self:sync_scroll_progress()
175+
end
176+
function SMODS.UIScrollBox:set_scroll_progress(t)
177+
self.scroll_progress = SMODS.merge_defaults(t, { x = 0, y = 0 })
178+
self:sync_scroll_offset()
179+
end
180+
function SMODS.UIScrollBox:sync_scroll(dt, init)
181+
if self.scroll_sync_mode == "progress" then
182+
self:sync_scroll_offset()
183+
elseif self.scroll_sync_mode == "offset" then
184+
self:sync_scroll_progress()
185+
end
186+
self.content_container.config.offset.x = -(self.scroll_offset.x or 0)
187+
self.content_container.config.offset.y = -(self.scroll_offset.y or 0)
188+
end
189+
function SMODS.UIScrollBox:update(dt)
190+
if self.scroll_args.scroll_move then
191+
self.scroll_args.scroll_move(self, dt)
192+
end
193+
self:sync_scroll(dt)
194+
UIBox.update(self, dt)
195+
end
196+
197+
--
198+
4199
function STR_UNPACK(str)
5200
local chunk, err = loadstring(str)
6201
if chunk then

0 commit comments

Comments
 (0)