Skip to content

Instantly share code, notes, and snippets.

@psychon
Created November 22, 2015 15:38
Show Gist options
  • Save psychon/2089ff521a51d9f2c77f to your computer and use it in GitHub Desktop.
Save psychon/2089ff521a51d9f2c77f to your computer and use it in GitHub Desktop.
Scroll layout
---------------------------------------------------------------------------
-- @author Uli Schlachter (based on ideas from Saleur Geoffrey)
-- @copyright 2015 Uli Schlachter
-- @release @AWESOME_VERSION@
-- @classmod wibox.layout.scroll
---------------------------------------------------------------------------
local cache = require("gears.cache")
local color = require("gears.color")
local matrix = require("gears.matrix")
local timer = require("gears.timer")
local hierarchy = require("wibox.hierarchy")
local base = require("wibox.widget.base")
local lgi = require("lgi")
local GLib = lgi.GLib
local cairo = lgi.cairo
local scroll = {}
local scroll_mt = { __index = scroll }
local function cleanup_context(context)
local skip = { wibox = true, drawable = true, client = true, position = true }
local res = {}
for k, v in pairs(context) do
if not skip[k] then
res[k] = v
end
end
return res
end
local function create_surface(context, widget, width, height, background, foreground)
local context = cleanup_context(context)
local surface = cairo.ImageSurface(cairo.Format.ARGB32, width, height)
local layouts = setmetatable({}, { __mode = "k" })
-- Create a widget hierarchy and update when needed
local hier
local dirty_area = cairo.Region.create_rectangle(cairo.RectangleInt{
x = 0, y = 0, width = width, height = height
})
local function do_pending_redraws(layout)
layouts[layout] = true
if dirty_area:is_empty() then
return
end
-- Update our surface
local cr = cairo.Context(surface)
for i = 0, dirty_area:num_rectangles() - 1 do
local rect = dirty_area:get_rectangle(i)
cr:rectangle(rect.x, rect.y, rect.width, rect.height)
end
dirty_area = cairo.Region.create()
cr:clip()
cr:save()
cr.operator = cairo.Operator.SOURCE
cr.source = background
cr:paint()
cr:restore()
cr.source = foreground
hier:draw(context, cr)
assert(cr.status == "SUCCESS", "Cairo context entered error state: " .. cr.status)
end
local function emit_layouts_redraw()
-- Make the scroll layouts redraw
for w in pairs(layouts) do
w:emit_signal("widget::redraw_needed")
end
end
local function redraw_callback(h, arg)
local m = h:get_matrix_to_device()
local x, y, width, height = matrix.transform_rectangle(m, h:get_draw_extents())
local x1, y1 = math.floor(x), math.floor(y)
local x2, y2 = math.ceil(x + width), math.ceil(y + height)
dirty_area:union_rectangle(cairo.RectangleInt{
x = x1, y = y1, width = x2 - x1, height = y2 - y1
})
emit_layouts_redraw()
end
local function layout_callback(h, arg)
hier:update(context, widget, width, height, dirty_area)
emit_layouts_redraw()
end
hier = hierarchy.new(context, widget, width, height, redraw_callback, layout_callback, nil)
return surface, do_pending_redraws
end
local surface_cache = cache.new(create_surface)
local function calculate_info(self, context, width, height)
local result = {}
assert(self.widget)
-- First, get the size of the widget (and the size of extra space)
local surface_width, surface_height = width, height
local extra_width, extra_height, extra = 0, 0, self.expand and self.extra_space or 0
local w, h
if self.dir == "h" then
w, h = base.fit_widget(self, context, self.widget, self.space_for_scrolling, height)
surface_width = w
extra_width = extra
else
w, h = base.fit_widget(self, context, self.widget, width, self.space_for_scrolling)
surface_height = h
extra_height = extra
end
result.fit_width, result.fit_height = w, h
if self.dir == "h" then
if self.max_size then
result.fit_width = math.min(w, self.max_size)
end
else
if self.max_size then
result.fit_height = math.min(h, self.max_size)
end
end
if w > width or h > height then
-- There is less space available than we need, we have to scroll
result.need_scroll = true
self:_need_scroll_redraw()
surface_width, surface_height = surface_width + extra_width, surface_height + extra_height
-- Only one of the caller needs the surface, hence the extra indirection
function result.get_surface()
local surface, do_pending_redraws = surface_cache:get(context,
self.widget, surface_width, surface_height,
self.background, self.foreground)
do_pending_redraws(self)
return surface
end
local x, y = 0, 0
local function get_scroll_offset(size, visible_size)
return self.step_function(self.timer:elapsed(), size, visible_size, self.speed, self.extra_space)
end
if self.dir == "h" then
x = -get_scroll_offset(surface_width - extra, width)
else
y = -get_scroll_offset(surface_height - extra, height)
end
result.first_x, result.first_y = x, y
-- Was the extra space already included elsewhere?
local extra = self.expand and 0 or self.extra_space
if self.dir == "h" then
x = x + surface_width + extra
else
y = y + surface_height + extra
end
result.second_x, result.second_y = x, y
else
result.need_scroll = false
end
return result
end
--- Set up the context for drawing children.
-- @param context The context in which we are drawn.
-- @param cr The cairo context to draw to.
-- @param width The available width.
-- @param height The available height.
function scroll:before_draw_children(context, cr, width, height)
if not self.widget then
return
end
local info = calculate_info(self, context, width, height)
if info.need_scroll then
return
end
cr:rectangle(0, 0, width, height)
cr.source = self.background
cr:fill()
cr.source = self.foreground
end
--- Draw this scrolling layout.
-- @param context The context in which we are drawn.
-- @param cr The cairo context to draw to.
-- @param width The available width.
-- @param height The available height.
function scroll:draw(context, cr, width, height)
if not self.widget then
return
end
local info = calculate_info(self, context, width, height)
if not info.need_scroll then
return
end
local surf = info.get_surface()
cr:set_source_surface(surf, info.first_x, info.first_y)
cr:paint()
cr:set_source_surface(surf, info.second_x, info.second_y)
cr:paint()
end
--- Fit the scroll layout into the given space.
-- @param context The context in which we are fit.
-- @param width The available width.
-- @param height The available height.
function scroll:fit(context, width, height)
if not self.widget then
return 0, 0
end
local info = calculate_info(self, context, width, height)
return info.fit_width, info.fit_height
end
--- Calculate the layout of the scroll layout.
-- @param context The context in which we are fit.
-- @param width The available width.
-- @param height The available height.
function scroll:layout(context, width, height)
if not self.widget then
return
end
local info = calculate_info(self, context, width, height)
if info.need_scroll then
return
end
return { base.place_widget_at(self.widget, 0, 0, width, height) }
end
function scroll:_need_scroll_redraw()
if not self.paused and not self.scroll_timer then
self.scroll_timer = timer.start_new(1 / self.fps, function()
self.scroll_timer = nil
self:emit_signal("widget::redraw_needed")
end)
end
end
function scroll:pause()
if self.paused then
return
end
self.paused = true
self.timer:stop()
end
function scroll:continue()
if not self.paused then
return
end
self.paused = false
self.timer:continue()
self:emit_signal("widget::redraw_needed")
end
function scroll:reset_scrolling()
self.timer:start()
if self.paused then
self.timer:stop()
end
end
--- Set the background to use
function scroll:set_bg(bg)
if bg then
self.background = color(bg)
else
self.background = nil
end
self:emit_signal("widget::redraw_needed")
end
--- Set the foreground to use
function scroll:set_fg(fg)
if fg then
self.foreground = color(fg)
else
self.foreground = nil
end
self:emit_signal("widget::redraw_needed")
end
function scroll:set_direction(dir)
if dir == self.dir then
return
end
if dir ~= "h" and dir ~= "v" then
error("Invalid direction, can only be 'h' or 'v'")
end
self.dir = dir
self:emit_signal("widget::layout_changed")
self:emit_signal("widget::redraw_needed")
end
function scroll:set_widget(widget)
if widget == self.widget then
return
end
if widget then
base.check_widget(widget)
end
self.widget = widget
self:emit_signal("widget::layout_changed")
self:emit_signal("widget::redraw_needed")
end
function scroll:set_expand(expand)
if expand == self.expand then
return
end
self.expand = expand
self:emit_signal("widget::redraw_needed")
end
function scroll:set_fps(fps)
if fps == self.fps then
return
end
self.fps = fps
end
function scroll:set_extra_space(extra_space)
if extra_space == self.extra_space then
return
end
self.extra_space = extra_space
self:emit_signal("widget::redraw_needed")
end
function scroll:set_speed(speed)
if speed == self.speed then
return
end
self.speed = speed
self:emit_signal("widget::redraw_needed")
end
function scroll:set_extra_space(extra_space)
if extra_space == self.extra_space then
return
end
self.extra_space = extra_space
self:emit_signal("widget::layout_changed")
end
function scroll:set_max_size(max_size)
if max_size == self.max_size then
return
end
self.max_size = max_size
self:emit_signal("widget::layout_changed")
end
function scroll:set_step_function(step_function)
-- Call the step functions once to see if it works
step_function(0, 42, 10, 10, 5)
if step_function == self.step_function then
return
end
self.step_function = step_function
self:emit_signal("widget::redraw_needed")
end
function scroll:set_space_for_scrolling(space_for_scrolling)
if space_for_scrolling == self.space_for_scrolling then
return
end
self.space_for_scrolling = space_for_scrolling
self:emit_signal("widget::layout_changed")
end
local function get_layout(dir, widget, fps, speed, extra_space, expand, max_size, step_function, space_for_scrolling)
local ret = base.make_widget()
ret.paused = false
ret.timer = GLib.Timer()
ret.scroll_timer = nil
setmetatable(ret, scroll_mt)
ret:set_direction(dir)
ret:set_widget(widget)
ret:set_fps(fps or 20)
ret:set_speed(speed or 10)
ret:set_extra_space(extra_space or 0)
ret:set_expand(expand)
ret:set_max_size(max_size)
ret:set_step_function(step_function or scroll.step_functions.linear_increase)
ret:set_space_for_scrolling(space_for_scrolling or 2^1024)
ret:set_bg("#ffffff")
ret:set_fg("#000000")
return ret
end
function scroll.horizontal(...)
return get_layout("h", ...)
end
function scroll.vertical(...)
return get_layout("v", ...)
end
scroll.step_functions = {}
function scroll.step_functions.linear_increase(elapsed, size, visible_size, speed, extra_space)
return (elapsed * speed) % (size + extra_space)
end
function scroll.step_functions.linear_decrease(elapsed, size, visible_size, speed, extra_space)
return (-elapsed * speed) % (size + extra_space)
end
function scroll.step_functions.linear_back_and_forth(elapsed, size, visible_size, speed, extra_space)
local state = ((elapsed * speed) % (2 * size)) / size
state = state <= 1 and state or 2 - state
return (size - visible_size) * state
end
function scroll.step_functions.waiting_back_and_forth(elapsed, size, visible_size, speed, extra_space)
local state = ((elapsed * speed) % (2 * size)) / size
local negate = false
if state > 1 then
negate = true
state = state - 1
end
if state < 1/5 or state > 4/5 then
-- One fifth of time, nothing moves
state = state < 1/5 and 0 or 1
else
state = (state - 1/5) * 5/3
if state < 1/3 then
-- In the first 1/3rd of time, do a quadratic increase in speed
state = 2 * state * state
elseif state < 2/3 then
-- In the center, do a linear increase. That means we need:
-- If state is 1/3, result is 2/9 = 2 * 1/3 * 1/3
-- If state is 2/3, result is 7/9 = 1 - 2 * (1 - 2/3) * (1 - 2/3)
state = 5/3*state - 3/9
else
-- In the last 1/3rd of time, do a quadratic decrease in speed
state = 1 - 2 * (1 - state) * (1 - state)
end
end
if negate then
state = 1 - state
end
return (size - visible_size) * state
end
return scroll
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment