awesome/lib/wibox/container/scroll.lua

578 lines
21 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---------------------------------------------------------------------------
-- This container scrolls its inner widget inside of the available space. An
-- example usage would be a text widget that displays information about the
-- currently playing song without using too much space for long song titles.
--
-- Please note that mouse events do not propagate to widgets inside of the
-- scroll container. Also, if this widget is causing too high CPU usage, you can
-- use @{set_fps} to make it update less often.
-- @usage
-- wibox.widget {
-- layout = wibox.container.scroll.horizontal,
-- max_size = 100,
-- step_function = wibox.container.scroll.step_functions
-- .waiting_nonlinear_back_and_forth,
-- speed = 100,
-- {
-- widget = wibox.widget.textbox,
-- text = "This is a " .. string.rep("very, ", 10) .. " very long text",
-- },
-- }
-- @author Uli Schlachter (based on ideas from Saleur Geoffrey)
-- @copyright 2015 Uli Schlachter
-- @containermod wibox.container.scroll
-- @supermodule wibox.widget.base
---------------------------------------------------------------------------
local cache = require("gears.cache")
local timer = require("gears.timer")
local hierarchy = require("wibox.hierarchy")
local base = require("wibox.widget.base")
local gtable = require("gears.table")
local lgi = require("lgi")
local GLib = lgi.GLib
local scroll = {}
local _need_scroll_redraw
-- "Strip" a context so that we can use it for our own drawing
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
-- Create a hierarchy (and some more stuff) for drawing the given widget. This
-- allows "some stuff" to be re-used instead of re-created all the time.
local hierarchy_cache = cache.new(function(context, widget, width, height)
context = cleanup_context(context)
local layouts = setmetatable({}, { __mode = "k" })
-- Create a widget hierarchy and update when needed
local hier
local function do_pending_updates(layout)
layouts[layout] = true
hier:update(context, widget, width, height, nil)
end
local function emit(signal)
-- Make the scroll layouts redraw
for w in pairs(layouts) do
w:emit_signal(signal)
end
end
local function redraw_callback()
emit("widget::redraw_needed")
end
local function layout_callback()
emit("widget::redraw_needed")
emit("widget::layout_changed")
end
hier = hierarchy.new(context, widget, width, height, redraw_callback, layout_callback, nil)
return hier, do_pending_updates, context
end)
--- Calculate all the information needed for scrolling.
-- @param self The instance of the scrolling layout.
-- @param context A widget context under which we are fit/drawn.
-- @param width The available width
-- @param height The available height
-- @return A table with the following entries
-- @field fit_width The width that should be returned from :fit
-- @field fit_height The height that should be returned from :fit
-- @field surface_width The width for showing the child widget
-- @field surface_height The height for showing the child widget
-- @field first_x The x offset for drawing the child the first time
-- @field first_y The y offset for drawing the child the first time
-- @field[opt] second_x The x offset for drawing the child the second time
-- @field[opt] second_y The y offset for drawing the child the second time
-- @field hierarchy The wibox.hierarchy instance representing "everything"
-- @field context The widget context for drawing the hierarchy
local function calculate_info(self, context, width, height)
local result = {}
assert(self._private.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._private.expand and self._private.extra_space or 0
local w, h
if self._private.dir == "h" then
w, h = base.fit_widget(self, context, self._private.widget, self._private.space_for_scrolling, height)
surface_width = w
extra_width = extra
else
w, h = base.fit_widget(self, context, self._private.widget, width, self._private.space_for_scrolling)
surface_height = h
extra_height = extra
end
result.fit_width, result.fit_height = w, h
if self._private.dir == "h" then
if self._private.max_size then
result.fit_width = math.min(w, self._private.max_size)
end
else
if self._private.max_size then
result.fit_height = math.min(h, self._private.max_size)
end
end
if w > width or h > height then
-- There is less space available than we need, we have to scroll
_need_scroll_redraw(self)
surface_width, surface_height = surface_width + extra_width, surface_height + extra_height
local x, y = 0, 0
local function get_scroll_offset(size, visible_size)
return self._private.step_function(self._private.timer:elapsed(),
size,
visible_size,
self._private.speed,
self._private.extra_space)
end
if self._private.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_spacer = self._private.expand and 0 or self._private.extra_space
if self._private.dir == "h" then
x = x + surface_width + extra_spacer
else
y = y + surface_height + extra_spacer
end
result.second_x, result.second_y = x, y
else
result.first_x, result.first_y = 0, 0
end
result.surface_width, result.surface_height = surface_width, surface_height
-- Get the hierarchy and subscribe ourselves to updates
local hier, do_pending_updates, ctx = hierarchy_cache:get(context,
self._private.widget, surface_width, surface_height)
result.hierarchy = hier
result.context = ctx
do_pending_updates(self)
return result
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._private.widget then
return
end
local info = calculate_info(self, context, width, height)
-- Draw the first instance of the child
cr:save()
cr:translate(info.first_x, info.first_y)
cr:rectangle(0, 0, info.surface_width, info.surface_height)
cr:clip()
info.hierarchy:draw(info.context, cr)
cr:restore()
-- If there is one, draw the second instance (same code as above, minus the
-- clip)
if info.second_x and info.second_y then
cr:translate(info.second_x, info.second_y)
cr:rectangle(0, 0, info.surface_width, info.surface_height)
cr:clip()
info.hierarchy:draw(info.context, cr)
end
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._private.widget then
return 0, 0
end
local info = calculate_info(self, context, width, height)
return info.fit_width, info.fit_height
end
-- Internal function used for triggering redraws for scrolling.
-- The purpose is to start a timer for redrawing the widget for scrolling.
-- Redrawing works by simply emitting the `widget::redraw_needed` signal.
-- Pausing is implemented in this function: We just don't start a timer.
-- This function must be idempotent (calling it multiple times right after
-- another does not make a difference).
_need_scroll_redraw = function(self)
if not self._private.paused and not self._private.scroll_timer then
self._private.scroll_timer = timer.start_new(1 / self._private.fps, function()
self._private.scroll_timer = nil
self:emit_signal("widget::redraw_needed")
end)
end
end
--- Pause the scrolling animation.
-- @method pause
-- @noreturn
-- @see continue
function scroll:pause()
if self._private.paused then
return
end
self._private.paused = true
self._private.timer:stop()
end
--- Continue the scrolling animation.
-- @method continue
-- @noreturn
-- @see pause
function scroll:continue()
if not self._private.paused then
return
end
self._private.paused = false
self._private.timer:continue()
self:emit_signal("widget::redraw_needed")
end
--- Reset the scrolling state to its initial condition.
-- For must scroll step functions, the effect of this function should be to
-- display the widget without any scrolling applied.
-- This function does not undo the effect of @{pause}.
-- @method reset_scrolling
-- @noreturn
function scroll:reset_scrolling()
self._private.timer:start()
if self._private.paused then
self._private.timer:stop()
end
end
--- Set the direction in which this widget scroll.
-- @method set_direction
-- @param dir Either "h" for horizontal scrolling or "v" for vertical scrolling
-- @noreturn
function scroll:set_direction(dir)
if dir == self._private.dir then
return
end
if dir ~= "h" and dir ~= "v" then
error("Invalid direction, can only be 'h' or 'v'")
end
self._private.dir = dir
self:emit_signal("widget::layout_changed")
self:emit_signal("widget::redraw_needed")
end
--- The widget to be scrolled.
-- @property widget
-- @tparam[opt=nil] widget|nil widget
function scroll:set_widget(widget)
if widget == self._private.widget then
return
end
local w = base.make_widget_from_value(widget)
if w then
base.check_widget(w)
end
self._private.widget = w
self:emit_signal("widget::layout_changed")
self:emit_signal("widget::redraw_needed")
end
function scroll:get_widget()
return self._private.widget
end
function scroll:get_children()
return {self._private.widget}
end
function scroll:set_children(children)
self:set_widget(children[1])
end
--- Specify the expand mode that is used for extra space.
-- @method set_expand
-- @tparam boolean expand If true, the widget is expanded to include the extra
-- space. If false, the extra space is simply left empty.
-- @noreturn
-- @see set_extra_space
function scroll:set_expand(expand)
if expand == self._private.expand then
return
end
self._private.expand = expand
self:emit_signal("widget::redraw_needed")
end
--- Set the number of frames per second that this widget should draw.
-- @method set_fps
-- @tparam number fps The number of frames per second
-- @noreturn
function scroll:set_fps(fps)
if fps == self._private.fps then
return
end
self._private.fps = fps
-- No signal needed: If we are scrolling, the next redraw will apply the new
-- FPS, else it obviously doesn't make a difference.
end
--- Set the amount of extra space that should be included in the scrolling. This
-- extra space will likely be left empty between repetitions of the widgets.
-- @method set_extra_space
-- @tparam number extra_space The amount of extra space
-- @noreturn
-- @see set_expand
function scroll:set_extra_space(extra_space)
if extra_space == self._private.extra_space then
return
end
self._private.extra_space = extra_space
self:emit_signal("widget::redraw_needed")
end
--- Set the speed of the scrolling animation. The exact meaning depends on the
-- step function that is used, but for the simplest step functions, this will be
-- in pixels per second.
-- @method set_speed
-- @tparam number speed The speed for the animation
-- @noreturn
function scroll:set_speed(speed)
if speed == self._private.speed then
return
end
self._private.speed = speed
self:emit_signal("widget::redraw_needed")
end
--- Set the maximum size of this widget in the direction set by
-- @{set_direction}. If the child widget is smaller than this size, no scrolling
-- is done. If the child widget is larger, then only this size will be visible
-- and the rest is made visible via scrolling.
-- @method set_max_size
-- @tparam number max_size The maximum size of this widget or nil for unlimited.
-- @noreturn
function scroll:set_max_size(max_size)
if max_size == self._private.max_size then
return
end
self._private.max_size = max_size
self:emit_signal("widget::layout_changed")
end
--- Set the step function that determines the exact behaviour of the scrolling
-- animation.
-- The step function is called with five arguments:
--
-- * The time in seconds since the state of the animation
-- * The size of the child widget
-- * The size of the visible part of the widget
-- * The speed of the animation. This should have a linear effect on this
-- function's behaviour.
-- * The extra space configured by @{set_extra_space}. This was not yet added to
-- the size of the child widget, but should likely be added to it in most
-- cases.
--
-- The step function should return a single number. This number is the offset at
-- which the widget is drawn and should be between 0 and `size+extra_space`.
-- @method set_step_function
-- @tparam function step_function A step function.
-- @noreturn
-- @see step_functions
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._private.step_function then
return
end
self._private.step_function = step_function
self:emit_signal("widget::redraw_needed")
end
--- Set an upper limit for the space for scrolling.
-- This restricts the child widget's maximal size.
-- @method set_space_for_scrolling
-- @tparam number space_for_scrolling The space for scrolling
-- @noreturn
function scroll:set_space_for_scrolling(space_for_scrolling)
if space_for_scrolling == self._private.space_for_scrolling then
return
end
self._private.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(nil, nil, {enable_properties = true})
ret._private.paused = false
ret._private.timer = GLib.Timer()
ret._private.scroll_timer = nil
gtable.crush(ret, scroll, true)
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)
return ret
end
--- Get a new horizontal scrolling container.
-- @constructorfct wibox.container.scroll.horizontal
-- @param[opt] widget The widget that should be scrolled
-- @param[opt=20] fps The number of frames per second
-- @param[opt=10] speed The speed of the animation
-- @param[opt=0] extra_space The amount of extra space to include
-- @tparam[opt=false] boolean expand Should the widget be expanded to include the
-- extra space?
-- @param[opt] max_size The maximum size of the child widget
-- @param[opt=step_functions.linear_increase] step_function The step function to be used
-- @param[opt=2^1024] space_for_scrolling The space for scrolling
function scroll.horizontal(widget, fps, speed, extra_space, expand, max_size, step_function, space_for_scrolling)
return get_layout("h", widget, fps, speed, extra_space, expand, max_size, step_function, space_for_scrolling)
end
--- Get a new vertical scrolling container.
-- @constructorfct wibox.container.scroll.vertical
-- @param[opt] widget The widget that should be scrolled
-- @param[opt=20] fps The number of frames per second
-- @param[opt=10] speed The speed of the animation
-- @param[opt=0] extra_space The amount of extra space to include
-- @tparam[opt=false] boolean expand Should the widget be expanded to include the
-- extra space?
-- @param[opt] max_size The maximum size of the child widget
-- @param[opt=step_functions.linear_increase] step_function The step function to be used
-- @param[opt=2^1024] space_for_scrolling The space for scrolling
function scroll.vertical(widget, fps, speed, extra_space, expand, max_size, step_function, space_for_scrolling)
return get_layout("v", widget, fps, speed, extra_space, expand, max_size, step_function, space_for_scrolling)
end
--- A selection of step functions
-- @see set_step_function
scroll.step_functions = {}
--- A step function that scrolls the widget in an increasing direction with
-- constant speed.
-- @callback scroll.step_functions.linear_increase
function scroll.step_functions.linear_increase(elapsed, size, _, speed, extra_space)
return (elapsed * speed) % (size + extra_space)
end
--- A step function that scrolls the widget in an decreasing direction with
-- constant speed.
-- @callback scroll.step_functions.linear_decrease
function scroll.step_functions.linear_decrease(elapsed, size, _, speed, extra_space)
return (-elapsed * speed) % (size + extra_space)
end
--- A step function that scrolls the widget to its end and back to its
-- beginning, then back to its end, etc. The speed is constant.
-- @callback scroll.step_functions.linear_back_and_forth
function scroll.step_functions.linear_back_and_forth(elapsed, size, visible_size, speed)
local state = ((elapsed * speed) % (2 * size)) / size
state = state <= 1 and state or 2 - state
return (size - visible_size) * state
end
--- A step function that scrolls the widget to its end and back to its
-- beginning, then back to its end, etc. The speed is null at the ends and
-- maximal in the middle.
-- @callback scroll.step_functions.nonlinear_back_and_forth
function scroll.step_functions.nonlinear_back_and_forth(elapsed, size, visible_size, speed)
local state = ((elapsed * speed) % (2 * size)) / size
local negate = false
if state > 1 then
negate = true
state = state - 1
end
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
if negate then
state = 1 - state
end
return (size - visible_size) * state
end
--- A step function that scrolls the widget to its end and back to its
-- beginning, then back to its end, etc. The speed is null at the ends and
-- maximal in the middle. At both ends the widget stands still for a moment.
-- @callback scroll.step_functions.waiting_nonlinear_back_and_forth
function scroll.step_functions.waiting_nonlinear_back_and_forth(elapsed, size, visible_size, speed)
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
--- A step function that scrolls the widget to its end and back to its
-- beginning, then back to its end, etc. The speed is null at the ends and
-- maximal in the middle. Its smoothed by the use of a cosine function.
-- @callback scroll.step_functions.smooth_back_and_forth
function scroll.step_functions.smooth_back_and_forth(elapsed, size, visible_size, speed)
local state = ((elapsed * speed) % (size)) / size
-- The cosine function is scaled to map [0,1] to [0,2π] then its output is
-- scaled from [1 -- -1 -- 1] to [0 -- 1 -- 0]
return (size - visible_size) * (0.5 - 0.5*math.cos(state*6.2832))
end
return scroll
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80