Created
November 22, 2015 15:38
-
-
Save psychon/2089ff521a51d9f2c77f to your computer and use it in GitHub Desktop.
Scroll layout
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--------------------------------------------------------------------------- | |
-- @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