diff --git a/widgets/init.lua b/widgets/init.lua index ca0be2e..34d817f 100644 --- a/widgets/init.lua +++ b/widgets/init.lua @@ -3,6 +3,7 @@ local wibox = require("wibox") wibox.layout.grid = require( "radical.widgets.grid" ) wibox.widget.checkbox = require( "radical.widgets.checkbox" ) +wibox.widget.slider = require( "radical.widgets.slider" ) return { checkbox = require( "radical.widgets.checkbox" ), diff --git a/widgets/slider.lua b/widgets/slider.lua new file mode 100644 index 0000000..edc0076 --- /dev/null +++ b/widgets/slider.lua @@ -0,0 +1,371 @@ +--------------------------------------------------------------------------- +-- An interactive mouse based slider widget. +-- +-- @author Grigory Mishchenko <grishkokot@gmail.com> +-- @author Emmanuel Lepage Vallee <elv1313@gmail.com> +-- @copyright 2015 Grigory Mishchenko, 2016 Emmanuel Lepage Vallee +-- @release @AWESOME_VERSION@ +-- @classmod wibox.widget.slider +--------------------------------------------------------------------------- + +local setmetatable = setmetatable +local type = type +local color = require("gears.color") +local util = require("awful.util") +local beautiful = require("beautiful") +local base = require("wibox.widget.base") +local shape = require("gears.shape") +local gmatrix = require("gears.matrix") +local capi = { + mouse = mouse, + mousegrabber = mousegrabber, + root = root, +} + +local slider = {mt={}} + +local properties = { + -- Handle + handle_shape = shape.rectangle, + handle_color = false, + handle_margins = {}, + handle_width = false, + handler_border_width = 0, + handler_border_color = false, + + -- Bar + bar_shape = shape.rectangle, + bar_height = false, + bar_color = false, + bar_margins = {}, + bar_border_width = 0, + bar_border_color = false, + + -- Content + value = 0, + minimum = 0, + maximum = 100, + + -- Other + cursor = "fleur", +} + +-- Create the accessors +for prop in pairs(properties) do + slider["set_"..prop] = function(self, value) + local changed = self._private[prop] ~= value + self._private[prop] = value + + if changed then + self:emit_signal("property::"..prop) + self:emit_signal("widget::redraw_needed") + end + end + + slider["get_"..prop] = function(self) + -- Ignoring the false's is on purpose + return self._private[prop] == nil + and properties[prop] + or self._private[prop] + end +end + +-- Add some validation to set_value +function slider:set_value(value) + value = math.min(value, self:get_maximum()) + value = math.max(value, self:get_minimum()) + local changed = self._private.value ~= value + + self._private.value = value + + if changed then + self:emit_signal( "property::value" ) + self:emit_signal( "widget::redraw_needed" ) + end +end + +local function get_extremums(self) + local min = self._private.minimum or properties.minimum + local max = self._private.maximum or properties.maximum + local interval = max - min + + return min, max, interval +end + +local function get_value(self, width, x) + local _, _, interval = get_extremums(self) + + return math.floor((x*interval)/width) +end + +function slider:draw(_, cr, width, height) + local bar_height = self._private.bar_height + + -- If there is no background, then skip this + local bar_color = self._private.bar_color + or beautiful.slider_bar_color + + if bar_color then + cr:set_source(color(bar_color)) + end + + local margins = self._private.bar_margins + or beautiful.slider_bar_margins + + local x_offset, right_margin, y_offset = 0, 0 + + if margins then + if type(margins) == "number" then + bar_height = bar_height or (height - 2*margins) + x_offset, y_offset = margins, margins + right_margin = margins + else + bar_height = bar_height or (height - margins.top - margins.bottom) + x_offset, y_offset = margins.left, margins.top + right_margin = margins.right + end + else + bar_height = bar_height or beautiful.slider_bar_height or height + y_offset = (height - bar_height)/2 + end + + + cr:translate(x_offset, y_offset) + + local bar_shape = self._private.bar_shape + or beautiful.slider_bar_shape + or properties.bar_shape + + local bar_border_width = self._private.bar_border_width + or beautiful.slider_bar_border_width + or properties.bar_border_width + + bar_shape(cr, width - x_offset - right_margin, bar_height or height) + + if bar_color then + if bar_border_width == 0 then + cr:fill() + else + cr:fill_preserve() + end + end + + -- Draw the bar border + if bar_border_width > 0 then + local bar_border_color = self._private.bar_border_color + or beautiful.slider_bar_border_color + or properties.bar_border_color + + if bar_border_color then + cr:save() + cr:set_source(color(bar_border_color)) + cr:stroke() + cr:restore() + else + cr:stroke() + end + end + + cr:translate(-x_offset, -y_offset) + + -- Paint the handle + local handle_color = self._private.handle_color + or beautiful.slider_handle_color + + -- It is ok if there is no color, it will be inherited + if handle_color then + cr:set_source(color(handle_color)) + end + + local handle_height, handle_width = height, self._private.handle_width + or beautiful.slider_handle_width + or height/2 + + local handle_shape = self._private.handle_shape + or beautiful.slider_handle_shape + or properties.handle_shape + + -- Lets get the margins for the handle + local margins = self._private.bar_margins + or beautiful.slider_bar_margins + + local x_offset, y_offset = 0, 0 + + if margins then + if type(margins) == "number" then + x_offset, y_offset = margins, margins + handle_width = handle_width - 2*margins + handle_height = handle_height - 2*margins + else + x_offset, y_offset = margins.left, margins.top + handle_width = handle_width - margins.left - margins.right + handle_height = handle_height - margins.top - margins.bottom + end + end + + local value = self._private.value or self._private.min or 0 + + -- Get the widget size back to it's non-transfored value + --local matrix = gmatrix.from_cairo_matrix(cr:get_matrix()) + local min, _, interval = get_extremums(self) + local rel_value = ((value-min)/interval) * (width-handle_width) + + cr:translate(x_offset + rel_value, y_offset) + + local handle_border_width = self._private.handle_border_width + or beautiful.slider_handle_border_width + or properties.handle_border_width + + handle_shape(cr, handle_width, handle_height) + + if handle_border_width > 0 then + cr:fill_preserve() + else + cr:fill() + end + + -- Draw the handle border + if handle_border_width > 0 then + local handle_border_color = self._private.handle_border_color + or beautiful.slider_handle_border_color + or properties.handle_border_color + + if handle_border_color then + cr:set_source(color(handle_border_color)) + end + + cr:stroke() + end +end + +function slider:fit(_, width, height) + -- Use all the space, this should be used with a constraint widget + return width, height +end + +local event = {} + +function event.mouse_enter(self) + self:emit_signal("widget::redraw_needed") --TODO + local cursor = self._private.cursor or "fleur" + capi.root.cursor(cursor) +end + +function event.mouse_leave(self) + self:emit_signal("widget::redraw_needed") --TODO +-- capi.root.cursor("fleur") +end + +local rect_order = {"x", "y", "width", "height"} + +-- Move the handle to the correct location +local function move_handle(self, x, y, geo) + -- Fix the X position when used in a rotate or mirror widget + + -- Remove the translation to bring {x, y} and the rect on the same origin + local rem_translate = geo.matrix_to_device*gmatrix.identity + rem_translate.x0, rem_translate.y0 = 0, 0 + + local rect = {rem_translate:transform_rectangle( + 0, + 0, + geo.width, + geo.height + )} + + -- Floating point precision is here both too much and not enough + for k, v in ipairs(rect) do + rect[rect_order[k]] = util.round(v) + end + + local px = rem_translate:transform_point(x, y) + + -- If the transformation moved the rectangle away from {0,0}, compensate + if rect.x < 0 then + px = rect.width + px + end + +-- --HACK Cairo 0,0 is top left, a matrix is bottom left, funny... + if util.round(rem_translate:transform_point(0, 100)) ~= 0 then + px = -(px - rect.width) + end + + self:set_value(get_value(self, rect.width, px)) + self:emit_signal("widget::redraw_needed") +end + +function event.mouse_press(self, x, y, button_id, _, geo) + if button_id ~= 1 then return end + + move_handle(self, x, y, geo) + + -- Get the "real" position of the widget + local wgeo = geo.drawable.drawable:geometry() + local wx = wgeo.x + geo.x + local wy = wgeo.y + geo.y + + capi.mousegrabber.run(function(mouse) + if not mouse.buttons[1] then + self:emit_signal("widget::redraw_needed") + return false + end + + -- Get the closest point to the widget + local abs_x, abs_y = mouse.x, mouse.y + abs_x = abs_x > (wx+geo.width) and wx+geo.width or ( + abs_x < wx and wx + ) or abs_x + abs_y = abs_y > (wy+geo.height) and wy+geo.height or ( + abs_y < wy and wy + ) or abs_y + + -- Get the relative point + local rx, ry = abs_x - wx, abs_y - wy + move_handle(self, rx, ry, geo) + + return true + end, self:get_cursor()) +end + +--- Create a slider widget. +-- @tparam[opt={}] table args +-- @function wibox.widget.slider +local function new(args) + local ret = base.make_widget(nil, nil, { + enable_properties = true, + }) + + util.table.crush(ret._private, args or {}) + + util.table.crush(ret, slider, true) + + -- Change the cusor and redraw (in case there is hover and pressed colors) + ret:connect_signal("mouse::enter" , event.mouse_enter) + ret:connect_signal("mouse::leave" , event.mouse_leave) + ret:connect_signal("button::press", event.mouse_press) + + return ret +end + +function slider.mt:__call(...) + return new(...) +end + +--FIXME This is still broken... +--[[w:setup{ + { + { + widget = wibox.widget.slider, + }, + direction = "east", + widget = wibox.container.rotate + }, + reflection = { + horizontal = true + }, + widget = wibox.container.mirror +}]] + +return setmetatable(slider, slider.mt) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80