-
Notifications
You must be signed in to change notification settings - Fork 0
/
overflow.lua
554 lines (455 loc) · 18 KB
/
overflow.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
---------------------------------------------------------------------------
-- A layout that allows its children to take more space than what's available
-- in the surrounding container. If the content does exceed the available
-- size, a scrollbar is added and scrolling behavior enabled.
--
--@DOC_wibox_layout_defaults_overflow_EXAMPLE@
-- @author Lucas Schwiderski
-- @copyright 2021 Lucas Schwiderski
-- @layoutmod wibox.layout.overflow
-- @supermodule wibox.layout.fixed
---------------------------------------------------------------------------
local base = require('wibox.widget.base')
local fixed = require('wibox.layout.fixed')
local separator = require('wibox.widget.separator')
local gtable = require('gears.table')
local gshape = require('gears.shape')
local gobject = require('gears.object')
local mousegrabber = mousegrabber
local overflow = { mt = {} }
-- Determine the required space to draw the layout's children and, if necessary,
-- the scrollbar.
function overflow:fit(context, orig_width, orig_height)
local widgets = self._private.widgets
local num_widgets = #widgets
if num_widgets < 1 then
return 0, 0
end
local width, height = orig_width, orig_height
local scrollbar_width = self._private.scrollbar_width
local scrollbar_enabled = self._private.scrollbar_enabled
local used_in_dir, used_max = 0, 0
local is_y = self._private.dir == "y"
local avail_in_dir = is_y and orig_height or orig_width
-- Set the direction covered by scrolling to the maximum value
-- to allow widgets to take as much space as they want.
if is_y then
height = math.huge
else
width = math.huge
end
-- First, determine widget sizes.
-- Only when the content doesn't fit and needs scrolling should
-- we reduce content size to make space for a scrollbar.
for _, widget in ipairs(widgets) do
local w, h = base.fit_widget(self, context, widget, width, height)
if is_y then
used_max = math.max(used_max, w)
used_in_dir = used_in_dir + h
else
used_max = math.max(used_max, h)
used_in_dir = used_in_dir + w
end
end
local spacing = self._private.spacing * (num_widgets - 1)
used_in_dir = used_in_dir + spacing
local need_scrollbar = scrollbar_enabled and used_in_dir > avail_in_dir
-- Even if `used_max == orig_(width|height)` already, `base.fit_widget`
-- will clamp return values, so we can "overextend" here.
if need_scrollbar then
used_max = used_max + scrollbar_width
end
if is_y then
return used_max, used_in_dir
else
return used_in_dir, used_max
end
end
-- Layout children, scrollbar and spacing widgets.
-- Only those widgets that are currently visible will be placed.
function overflow:layout(context, orig_width, orig_height)
local result = {}
local is_y = self._private.dir == "y"
local widgets = self._private.widgets
local avail_in_dir = is_y and orig_height or orig_width
local scrollbar_width = self._private.scrollbar_width
local scrollbar_enabled = self._private.scrollbar_enabled
local scrollbar_position = self._private.scrollbar_position
local width, height = orig_width, orig_height
local widget_x, widget_y = 0, 0
local used_in_dir, used_max = 0, 0
-- Set the direction covered by scrolling to the maximum value
-- to allow widgets to take as much space as they want.
if is_y then
height = math.huge
else
width = math.huge
end
-- First, determine widget sizes.
-- Only when the content doesn't fit and needs scrolling should
-- we reduce content size to make space for a scrollbar.
for _, widget in pairs(widgets) do
local w, h = base.fit_widget(self, context, widget, width, height)
if is_y then
used_max = math.max(used_max, w)
used_in_dir = used_in_dir + h
else
used_max = math.max(used_max, h)
used_in_dir = used_in_dir + w
end
end
used_in_dir = used_in_dir + self._private.spacing * (#widgets-1)
-- Save size for scrolling behavior
self._private.avail_in_dir = avail_in_dir
self._private.used_in_dir = used_in_dir
local need_scrollbar = used_in_dir > avail_in_dir and scrollbar_enabled
local scroll_position = self._private.scroll_factor
if need_scrollbar then
local scrollbar_widget = self._private.scrollbar_widget
local bar_x, bar_y = 0, 0
local bar_w, bar_h
-- The percentage of how much of the content can be visible within
-- the available space
local visible_percent = avail_in_dir / used_in_dir
-- Make scrollbar length reflect `visible_percent`
-- TODO: Apply a default minimum length
local bar_length = math.floor(visible_percent * avail_in_dir)
local bar_pos = (avail_in_dir - bar_length) * self._private.scroll_factor
if is_y then
bar_w, bar_h = base.fit_widget(self, context, scrollbar_widget, scrollbar_width, bar_length)
bar_y = bar_pos
if scrollbar_position == "left" then
widget_x = widget_x + bar_w
elseif scrollbar_position == "right" then
bar_x = orig_width - bar_w
end
self._private.bar_length = bar_h
width = width - bar_w
else
bar_w, bar_h = base.fit_widget(self, context, scrollbar_widget, bar_length, scrollbar_width)
bar_x = bar_pos
if scrollbar_position == "top" then
widget_y = widget_y + bar_h
elseif scrollbar_position == "bottom" then
bar_y = orig_height - bar_h
end
self._private.bar_length = bar_w
height = height - bar_h
end
table.insert(result, base.place_widget_at(
scrollbar_widget,
math.floor(bar_x),
math.floor(bar_y),
math.floor(bar_w),
math.floor(bar_h)
))
end
local pos, spacing = 0, self._private.spacing
local interval = used_in_dir - avail_in_dir
local spacing_widget = self._private.spacing_widget
if spacing_widget then
if is_y then
local _
_, spacing = base.fit_widget(self, context, spacing_widget, width, spacing)
else
spacing = base.fit_widget(self, context, spacing_widget, spacing, height)
end
end
for i, w in ipairs(widgets) do
local content_x, content_y
local content_w, content_h = base.fit_widget(self, context, w, width, height)
-- When scrolling down, the content itself moves up -> substract
local scrolled_pos = pos - (scroll_position * interval)
-- Stop processing completely once we're passed the visible portion
if scrolled_pos > avail_in_dir then
break
end
if is_y then
content_x, content_y = widget_x, scrolled_pos
pos = pos + content_h + spacing
if self._private.fill_space then
content_w = width
end
else
content_x, content_y = scrolled_pos, widget_y
pos = pos + content_w + spacing
if self._private.fill_space then
content_h = height
end
end
local is_in_view = is_y
and (scrolled_pos + content_h > 0)
or (scrolled_pos + content_w > 0)
if is_in_view then
-- Add the spacing widget, but not before the first widget
if i > 1 and spacing_widget then
table.insert(result, base.place_widget_at(
spacing_widget,
-- The way how spacing is added for regular widgets
-- and the `spacing_widget` is disconnected:
-- The offset for regular widgets is added to `pos` one
-- iteration _before_ the one where the widget is actually
-- placed.
-- Because of that, the placement for the spacing widget
-- needs to substract that offset to be placed right after
-- the previous regular widget.
math.floor(is_y and content_x or (content_x - spacing)),
math.floor(is_y and (content_y - spacing) or content_y),
math.floor(is_y and content_w or spacing),
math.floor(is_y and spacing or content_h)
))
end
table.insert(result, base.place_widget_at(
w,
math.floor(content_x),
math.floor(content_y),
math.floor(content_w),
math.floor(content_h)
))
end
end
return result
end
function overflow:before_draw_children(_, cr, width, height)
-- Clip drawing for children to the space we're allowed to draw in
cr:rectangle(0, 0, width, height)
cr:clip()
end
--- The amount of units to advance per scroll event.
--
-- This affects calls to `scroll` and the default mouse wheel handler.
--
-- The default is `10`.
--
-- @property step
-- @tparam number step The step size.
function overflow:set_step(step)
self._private.step = step
-- We don't need to emit enything here, since changing step only really
-- takes effect the next time the user scrolls
end
--- Scroll the layout's content by `amount * step`.
--
-- A positive amount scrolls down/right, a negative amount scrolls up/left.
--
-- The amount of units scrolled is affected by `step`.
--
-- @method overflow:scroll
-- @tparam number amount The amount to scroll by.
-- @emits property::overflow::scroll_factor
-- @emitstparam property::overflow::scroll_factor number scroll_factor The new
-- scroll factor.
-- @emits widget::layout_changed
-- @emits widget::redraw_needed
function overflow:scroll(amount)
if amount == 0 then
return
end
local interval = self._private.used_in_dir
local delta = self._private.step / interval
local factor = self._private.scroll_factor + (delta * amount)
self:set_scroll_factor(factor)
end
--- The scroll factor.
--
-- The scroll factor represents how far the layout's content is currently
-- scrolled. It is represented as a fraction from `0` to `1`, where `0` is the
-- start of the content and `1` is the end.
--
-- @property scroll_factor
-- @tparam number scroll_factor The scroll factor.
-- @propemits true false
function overflow:set_scroll_factor(factor)
local current = self._private.scroll_factor
local interval = self._private.used_in_dir - self._private.avail_in_dir
if current == factor
-- the content takes less space than what is available, i.e. everything
-- is already visible
or interval <= 0
-- the scroll factor is out of range
or (current <= 0 and factor < 0)
or (current >= 1 and factor > 1) then
return
end
self._private.scroll_factor = math.min(1, math.max(factor, 0))
self:emit_signal("widget::layout_changed")
self:emit_signal("property::scroll_factor", factor)
end
function overflow:get_scroll_factor()
return self._private.scroll_factor
end
--- The scrollbar width.
--
-- For horizontal scrollbars, this is the scrollbar height
--
-- The default is `5`.
--
--@DOC_wibox_layout_overflow_scrollbar_width_EXAMPLE@
--
-- @property scrollbar_width
-- @tparam number scrollbar_width The scrollbar width.
-- @propemits true false
function overflow:set_scrollbar_width(width)
if self._private.scrollbar_width == width then
return
end
self._private.scrollbar_width = width
self:emit_signal("widget::layout_changed")
self:emit_signal("property::scrollbar_width", width)
end
--- The scrollbar position.
--
-- For horizontal scrollbars, this can be `"top"` or `"bottom"`,
-- for vertical scrollbars this can be `"left"` or `"right"`.
-- The default is `"right"`/`"bottom"`.
--
--@DOC_wibox_layout_overflow_scrollbar_position_EXAMPLE@
--
-- @property scrollbar_position
-- @tparam string scrollbar_position The scrollbar position.
-- @propemits true false
function overflow:set_scrollbar_position(position)
if self._private.scrollbar_position == position then
return
end
self._private.scrollbar_position = position
self:emit_signal("widget::layout_changed")
self:emit_signal("property::scrollbar_position", position)
end
function overflow:get_scrollbar_position()
return self._private.scrollbar_position
end
--- The scrollbar visibility.
--
-- If this is set to `false`, no scrollbar will be rendered, even if the layout's
-- content overflows. Mouse wheel scrolling will work regardless.
--
-- The default is `true`.
--
-- @property scrollbar_enabled
-- @tparam boolean scrollbar_enabled The scrollbar visibility.
-- @propemits true false
function overflow:set_scrollbar_enabled(enabled)
if self._private.scrollbar_enabled == enabled then
return
end
self._private.scrollbar_enabled = enabled
self:emit_signal("widget::layout_changed")
self:emit_signal("property::scrollbar_enabled", enabled)
end
function overflow:get_scrollbar_enabled()
return self._private.scrollbar_enabled
end
-- Wraps a callback function for `mousegrabber` that is capable of
-- updating the scroll factor.
local function build_grabber(container, initial_x, initial_y, geo)
local is_y = container._private.dir == "y"
local bar_interval = container._private.avail_in_dir - container._private.bar_length
local start_pos = container._private.scroll_factor * bar_interval
local start = is_y and initial_y or initial_x
-- Calculate a matrix transforming from screen coordinates into widget
-- coordinates.
-- This is required for mouse movement to work when the widget has been
-- transformed by something like `wibox.container.rotate`.
local matrix_from_device = geo.hierarchy:get_matrix_from_device()
local wgeo = geo.drawable.drawable:geometry()
local matrix = matrix_from_device:translate(-wgeo.x, -wgeo.y)
return function(mouse)
if not mouse.buttons[1] then
return false
end
local x, y = matrix:transform_point(mouse.x, mouse.y)
local pos = is_y and y or x
container:set_scroll_factor((start_pos + (pos - start)) / bar_interval)
return true
end
end
-- Applies a mouse button signal using `build_grabber` to a scrollbar widget.
local function apply_scrollbar_mouse_signal(container, w)
w:connect_signal('button::press', function(_, x, y, button_id, _, geo)
if button_id ~= 1 then
return
end
mousegrabber.run(build_grabber(container, x, y, geo), "fleur")
end)
end
--- The scrollbar widget.
-- This widget is rendered as the scrollbar element.
--
-- The default is `wibox.widget.separator{ shape = gears.shape.rectangle }`.
--
--@DOC_wibox_layout_overflow_scrollbar_widget_EXAMPLE@
--
-- @property scrollbar_widget
-- @tparam widget scrollbar_widget The scrollbar widget.
-- @propemits true false
function overflow:set_scrollbar_widget(widget)
local w = base.make_widget_from_value(widget)
apply_scrollbar_mouse_signal(self, w)
self._private.scrollbar_widget = w
self:emit_signal("widget::layout_changed")
self:emit_signal("property::scrollbar_widget", widget)
end
function overflow:get_scrollbar_widget()
return self._private.scrollbar_widget
end
function overflow:reset()
self._private.widgets = {}
self._private.scroll_factor = 0
local scrollbar_widget = separator({ shape = gshape.rounded_bar })
apply_scrollbar_mouse_signal(self, scrollbar_widget)
self._private.scrollbar_widget = scrollbar_widget
self:emit_signal("widget::layout_changed")
self:emit_signal("widget::reset")
self:emit_signal("widget::reseted")
end
local function new(dir, ...)
local ret = fixed[dir](...)
gtable.crush(ret, overflow, true)
ret.widget_name = gobject.modulename(2)
-- Tell the widget system to prevent clicks outside the layout's extends
-- to register with child widgets, even if they actually extend that far.
-- This prevents triggering button presses on hidden/clipped widgets.
ret.clip_child_extends = true
-- Manually set the scroll factor here. We don't know the bounding size yet.
ret._private.scroll_factor = 0
-- Apply defaults. Bypass setters to avoid signals.
ret._private.step = 50
ret._private.fill_space = true
ret._private.scrollbar_width = 5
ret._private.scrollbar_enabled = true
ret._private.scrollbar_position = dir == "vertical" and "right" or "bottom"
local scrollbar_widget = separator({ shape = gshape.rectangle })
apply_scrollbar_mouse_signal(ret, scrollbar_widget)
ret._private.scrollbar_widget = scrollbar_widget
ret:connect_signal('button::press', function(self, _, _, button)
if button == 4 then
self:scroll(-1)
elseif button == 5 then
self:scroll(1)
end
end)
return ret
end
--- Returns a new horizontal overflow layout.
-- Child widgets are placed similar to `wibox.layout.fixed`, except that
-- they may take as much width as they want. If the total width of all child
-- widgets exceeds the width available whithin the layout's outer container
-- a scrollbar will be added and scrolling behavior enabled.
-- @tparam widget ... Widgets that should be added to the layout.
-- @constructorfct wibox.layout.overflow.horizontal
function overflow.horizontal(...)
return new("horizontal", ...)
end
--- Returns a new vertical overflow layout.
-- Child widgets are placed similar to `wibox.layout.fixed`, except that
-- they may take as much height as they want. If the total height of all child
-- widgets exceeds the height available whithin the layout's outer container
-- a scrollbar will be added and scrolling behavior enabled.
-- @tparam widget ... Widgets that should be added to the layout.
-- @constructorfct wibox.layout.overflow.vertical
function overflow.vertical(...)
return new("vertical", ...)
end
return setmetatable(overflow, overflow.mt)
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80