diff --git a/awesome/src/lib/overflow_widget/overflow.lua b/awesome/src/lib/overflow_widget/overflow.lua new file mode 100644 index 0000000..a4511bb --- /dev/null +++ b/awesome/src/lib/overflow_widget/overflow.lua @@ -0,0 +1,616 @@ +--------------------------------------------------------------------------- +-- 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 +--------------------------------------------------------------------------- + +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 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_in_dir = used_in_dir + w + used_max = math.max(used_max, h) + end + end + + local spacing = self._private.spacing * (num_widgets - 1) + used_in_dir = used_in_dir + spacing + + local need_scrollbar = used_in_dir > avail_in_dir and scrollbar_enabled + + -- If the direction perpendicular to scrolling (e.g. width in vertical + -- scrolling) is not fully covered by any of the widgets, we can add our + -- scrollbar width to that value. Otherwise widget size will be reduced + -- during `layout` to make space for the scrollbar. + if need_scrollbar + and ( + (is_y and used_max < orig_width) + or (not is_y and used_max < orig_height) + ) 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_in_dir = used_in_dir + w + used_max = math.max(used_max, h) + 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.position + + 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.position + + 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 pairs(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. +-- @see set_step + +--- Set the step size. +-- +-- @method overflow:set_step +-- @tparam number step The step size. +-- @see step +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 scroll down/right, a negative amount scrolls up/left. +-- +-- @method overflow:scroll +-- @tparam number amount The amount to scroll by. +-- @emits property::overflow::position +-- @emitstparam property::overflow::position number position The new position. +-- @emits widget::layout_changed +-- @emits widget::redraw_needed +-- @see step +function overflow:scroll(amount) + if amount == 0 then + return + end + local interval = self._private.used_in_dir + local delta = self._private.step / interval + + local pos = self._private.position + (delta * amount) + self:set_position(pos) +end + +--- The scroll position. +-- The position is represented as a fraction from `0` to `1`. +-- +-- @property position +-- @tparam number position The position. +-- @propemits true false +-- @see set_position + +--- Set the current scroll position. +-- +-- @method overflow:set_position +-- @tparam number position The new position. +-- @propemits true false +-- @emits widget::layout_changed +-- @emits widget::redraw_needed +-- @see position +function overflow:set_position(pos) + local current = self._private.position + local interval = self._private.used_in_dir - self._private.avail_in_dir + if current == pos + -- the content takes less space than what is available, i.e. everything + -- is already visible + or interval <= 0 + -- the position is out of range + or (current <= 0 and pos < 0) + or (current >= 1 and pos > 1) then + return + end + + self._private.position = math.min(1, math.max(pos, 0)) + + self:emit_signal("widget::layout_changed") + self:emit_signal("property::position", pos) +end + +--- Get the current scroll position. +-- +-- @method overflow:get_position +-- @treturn number position The current position. +-- @see position +function overflow:get_position() + return self._private.position +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 +-- @see set_scrollbar_width + +--- Set the scrollbar width. +-- +-- @method overflow:set_scrollbar_width +-- @tparam number scrollbar_width The new scrollbar width. +-- @propemits true false +-- @emits widget::layout_changed +-- @emits widget::redraw_needed +-- @see scrollbar_width +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 `"left"`/`"bottom"`. +-- +--@DOC_wibox_layout_overflow_scrollbar_position_EXAMPLE@ +-- +-- @property scrollbar_position +-- @tparam string scrollbar_position The scrollbar position. +-- @propemits true false +-- @see set_scrollbar_position + +--- Set the scrollbar position. +-- +-- @method overflow:set_scrollbar_position +-- @tparam string scrollbar_position The new scrollbar position. +-- @propemits true false +-- @emits widget::layout_changed +-- @emits widget::redraw_needed +-- @see scrollbar_position +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 + +--- 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 +-- @see set_scrollbar_enabled + +--- Enable or disable the scrollbar visibility. +-- +-- @method overflow:set_scrollbar_enabled +-- @tparam boolean scrollbar_enabled The new scrollbar visibility. +-- @propemits true false +-- @emits widget::layout_changed +-- @emits widget::redraw_needed +-- @see scrollbar_enabled +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 + +-- Wraps a callback function for `mousegrabber` that is capable of +-- updating the scroll position. +local function build_grabber(container) + local is_y = container._private.dir == "y" + local bar_interval = container._private.avail_in_dir - container._private.bar_length + local start_pos = container._private.position * bar_interval + local coords = mouse.coords() + local start = is_y and coords.y or coords.x + + return function(mouse) + if not mouse.buttons[1] then + return false + end + + local pos = is_y and mouse.y or mouse.x + container:set_position((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(_, _, _, button_id) + if button_id ~= 1 then + return + end + mousegrabber.run(build_grabber(container), "fleur") + end) +end + +--- The scrollbar widget. +-- This widget is rendered as the scrollbar element. +-- +-- The default is `awful.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 +-- @see set_scrollbar_widget + +--- Set the scrollbar widget. +-- +-- This will also apply the mouse button handler. +-- +-- @method overflow:set_scrollbar_widget +-- @tparam widget scrollbar_widget The new scrollbar widget. +-- @propemits true false +-- @emits widget::layout_changed +-- @see scrollbar_widget +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 + +local function new(dir, ...) + local ret = fixed[dir](...) + + gtable.crush(ret, overflow, true) + ret.widget_name = gobject.modulename(2) + + -- Manually set the position here. We don't know the bounding size yet. + ret._private.position = 0 + + -- Apply defaults. Bypass setters to avoid signals. + ret._private.step = 10 + 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 + if self.scroll_speed == nil or self.scroll_speed <= 0 then + self:scroll(-1) + else + self:scroll(-1 * self.scroll_speed) + end + elseif button == 5 then + if self.scroll_speed == nil or self.scroll_speed <= 0 then + self:scroll(1) + else + self:scroll(1 * self.scroll_speed) + end + 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.fixed.horizontal +function overflow.vertical(...) + return new("vertical", ...) +end + +--- Add spacing between each layout widgets. +-- +-- This behaves just like in `wibox.layout.fixed`: +-- +--@DOC_wibox_layout_fixed_spacing_EXAMPLE@ +-- +-- @property spacing +-- @tparam number spacing Spacing between widgets. +-- @propemits true false +-- @see wibox.layout.fixed + + +--- The widget used to fill the spacing between the layout elements. +-- By default, no widget is used. +-- +-- This behaves just like in `wibox.layout.fixed`: +-- +--@DOC_wibox_layout_fixed_spacing_widget_EXAMPLE@ +-- +-- @property spacing_widget +-- @tparam widget spacing_widget +-- @propemits true false +-- @see wibox.layout.fixed + + +--- Set the layout's fill_space property. +-- +-- If this property is `true`, widgets +-- take all space in the non-scrolling directing (e.g. `width` for vertical +-- scrolling). If `false`, they will only take as much as they need for their +-- content. +-- +-- The default is `true`. +-- +--@DOC_wibox_layout_overflow_fill_space_EXAMPLE@ +-- +-- @property fill_space +-- @tparam boolean fill_space +-- @propemits true false + + +--@DOC_fixed_COMMON@ + +--@DOC_widget_COMMON@ + +--@DOC_object_COMMON@ + +return setmetatable(overflow, overflow.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/awesome/src/modules/notification-center/notification_list.lua b/awesome/src/modules/notification-center/notification_list.lua index 7ee0f86..8b729b5 100644 --- a/awesome/src/modules/notification-center/notification_list.lua +++ b/awesome/src/modules/notification-center/notification_list.lua @@ -15,7 +15,7 @@ local icondir = awful.util.getdir("config") .. "src/assets/icons/notifications/" local nl = {} -nl.notification_list = { layout = wibox.layout.fixed.vertical, spacing = dpi(20) } +nl.notification_list = { layout = require("src.lib.overflow_widget.overflow").vertical, scrollbar_width = 0, scroll_speed = 10, spacing = dpi(20) } -- @param {table} notification -- @return {widget} notifications_list