radical/radial.lua

448 lines
15 KiB
Lua

local setmetatable = setmetatable
local ipairs,pairs = ipairs,pairs
local table,print = table,print
local math,string = math,string
local unpack,type = unpack,type
local base = require( "radical.base" )
local awful = require( "awful" )
local util = require( "awful.util" )
local button = require( "awful.button" )
local checkbox = require( "radical.widgets.checkbox" )
local beautiful = require( "beautiful" )
local naughty = require( "naughty" )
local wibox = require( "wibox" )
local tag = require( "awful.tag" )
local color = require( "gears.color" )
local cairo = require( "lgi" ).cairo
local shape = require( "gears.shape" )
local default = {width=90,height=30,radius=40,base_radius=60}
local capi = { mouse = mouse, mousegrabber = mousegrabber }
local module={}
-- Generate a cached pixmap starting at pi/2-(2*pi / seg_count)
-- This mask is then rotated in place where desired
-- the reason for this is to keep the code simple and allow
-- a menu rotate "animation" with the scroll wheel
local function gen_text_mask(data, layer, segs, text)
-- Step 1: compute the minimal size
--TODO limit to the radius
local h = (default.base_radius + layer*default.radius) * (segs < 2 and 2 or 1)
-- I let the reader make the proof of this: good luck with that
local dr = (math.pi*2)/segs
local w = segs <= 2 and data.width or 2*math.abs(math.sin((dr)/3)*(data.width/2)) + 3
-- Step 2: Create the surface
local img = cairo.ImageSurface(cairo.Format.ARGB32, w, h)
local cr = cairo.Context(img)
-- cr:set_source_rgba(1,0,0,1)
-- cr:paint()
-- cr:set_source_rgba(1,1,1,1)
local border_width= 4
local start_angle = -(math.pi/2) - dr/2
local end_angle = -(math.pi/2) + dr/2
local cur_angle = start_angle
local cur_rad = h-border_width
cr:set_source_rgba(1,1,1,1)
cr:select_font_face("monospace")
local matrix = cairo.Matrix()
cairo.Matrix.init_translate(matrix,w/2,h)
for i=1,text:len() do
local c = text:sub(i,i)
-- Step 4: Create the transformation matrix
-- TODO use composite
local matrix12 = cairo.Matrix()
local ex = cr:text_extents(c)
cairo.Matrix.init_rotate(matrix12, cur_angle+math.pi/2)
matrix12:translate(-ex.width/2,ex.height/2)
local trext = cairo.Matrix()
cairo.Matrix.init_translate(trext,
cur_rad*math.cos(cur_angle),
cur_rad*math.sin(cur_angle)
)
local res = cairo.Matrix()
res:multiply(trext,matrix)
local res2 = cairo.Matrix()
res2:multiply(matrix12,res)
cr:set_matrix(res2)
-- Step 4: Paint
cr:text_path(c)
cr:fill()
-- Step 5: update the angle
cur_angle = cur_angle + 0.05 --TODO use trigonometry to copute this
if cur_angle > end_angle then
cur_angle = start_angle
cur_rad = cur_rad - border_width - ex.height
end
end
return img
end
function module.radial_client_select(args)
--Settings
local args = args or {}
local data = {width=400,height=400,layers={},compose={}}
local screen = args.screen or mouse.screen
local height = args.height or default.height
local width = args.widget or default.width
local function position_indicator_layer()
if not data.indicator or data.angle_cache ~= data.angle then
local angle,tan = data.angle or 0,data.tan or 0
if not data.indicator then
data.indicator = {}
data.indicator.img = cairo.ImageSurface(cairo.Format.ARGB32, data.width, data.height)
data.indicator.cr = cairo.Context(data.indicator.img)
else
data.indicator.cr:set_operator(cairo.Operator.CLEAR)
data.indicator.cr:paint()
data.indicator.cr:set_operator(cairo.Operator.SOURCE)
end
data.indicator.cr:set_source_rgb(1,0,0)
-- The Inner dot around the dotted circle
local littledot_rad = 5
local dot = shape.transform(shape.circle) : translate(
data.width/2 + (default.base_radius-20)*math.cos(angle) -littledot_rad,
data.width/2 + (default.base_radius-20)*math.sin(angle) -littledot_rad
)
dot(data.indicator.cr, 2*littledot_rad, 2*littledot_rad)
data.indicator.cr:fill()
-- The little arc on top of the border
data.indicator.cr:set_line_width(4)
data.indicator.cr:arc( data.width/2,data.height/2,default.radius + default.base_radius ,angle-0.15,angle+0.15 )
data.indicator.cr:stroke()
-- The Big red dot TODO make it move
dot = shape.transform(shape.circle) : translate(
data.width/2+170 -2*littledot_rad,
data.height/2 -2*littledot_rad
)
dot(data.indicator.cr, 4*littledot_rad, 4*littledot_rad)
data.indicator.cr:fill()
data.angle_cache = data.angle
end
return data.indicator.img
end
local function create_inner_circle()
if not data.inner then
data.inner = {}
data.inner.img = cairo.ImageSurface(cairo.Format.ARGB32, data.width, data.height)
data.inner.cr = cairo.Context(data.inner.img)
data.inner.cr:set_line_width(3)
data.inner.cr:set_source_rgb(0.9,0.9,0.9)
data.inner.cr:arc ( data.width/2,data.height/2,default.base_radius,0,2*math.pi )
data.inner.cr:close_path()
data.inner.cr:stroke()
data.inner.cr:arc ( data.width/2,data.height/2,default.base_radius-20,0,2*math.pi )
data.inner.cr:close_path()
data.inner.cr:set_dash({10,4},1)
data.inner.cr:stroke()
end
return data.inner.img
end
local function clear_center(layer)
local dat_layer = data.layers[layer]
local rad = default.base_radius+(layer-1)*default.radius
dat_layer.cr:set_operator(cairo.Operator.CLEAR)
dat_layer.cr:set_source_rgba ( 0 , 0 , 0,1 )
dat_layer.cr:move_to ( data.width/2, data.height/2 )
dat_layer.cr:arc (data.width/2,data.height/2,rad,0,2*math.pi )
dat_layer.cr:fill ( )
end
local function gen_arc(layer)
data.layers[layer].position = (data.layers[layer].position or 0) + 1
local sef_count = #data.layers[layer].content
local position = data.layers[layer].position
local dat_layer = data.layers[layer]
local outer_radius = layer*default.radius + default.base_radius
local inner_radius = outer_radius - default.radius
local start_angle = ((2*math.pi)/sef_count)*(position-1)
local end_angle = ((2*math.pi)/sef_count)*(position)
dat_layer.cr:set_operator(cairo.Operator.SOURCE)
if data.layers[layer].selected == position then
dat_layer.cr:set_source ( color(beautiful.fg_focus) )
else
dat_layer.cr:set_source_rgb((5+(21-5)/sef_count*position)/256,(10+(119-10)/sef_count*position)/256,(27+(209-27)/sef_count*position)/256)
end
dat_layer.cr:move_to ( data.width/2, data.height/2 )
dat_layer.cr:arc ( data.width/2,data.height/2,outer_radius,start_angle,end_angle )
dat_layer.cr:fill_preserve ( )
dat_layer.cr:set_source_rgb(90/256,51/256,83/256)
dat_layer.cr:close_path()
dat_layer.cr:stroke()
clear_center(layer)
end
local function draw_text(cr,text, start_angle, end_angle, layer)
if not text then return end
local img2 = cairo.ImageSurface(cairo.Format.ARGB32, 20, 20)
local cr2 = cairo.Context(img2)
local level =0
local step = (2*math.pi)/(((math.pi*2*(default.base_radius + default.radius*layer - 3 - level*12))/4)*0.65) --relation between arc and char width
local testAngle = start_angle + 0.05
cr2:select_font_face("monospace")
local img = gen_text_mask(data,layer,#data.layers[layer].content,text)
cr:set_source_surface(img,100,60)
cr:paint()
-- for i=1,text:len() do
-- cr2:set_operator(cairo.Operator.CLEAR)
-- cr2:paint()
-- cr2:set_operator(cairo.Operator.SOURCE)
-- cr2:set_source_rgb(1,1,1)
-- cr2:move_to(0,10)
-- cr2:text_path(text:sub(i,i))
-- cr2:fill()
-- local matrix12 = cairo.Matrix()
-- cairo.Matrix.init_rotate(matrix12, -testAngle )
-- matrix12:translate(-data.width/2+(default.base_radius + default.radius*layer - 3 - level*12)*(math.sin( - testAngle)),-data.height/2+(default.base_radius + default.radius*layer - 3 - level*12)*(math.cos( -testAngle)))
-- local pattern = cairo.Pattern.create_for_surface(img2,20,20)
-- pattern:set_matrix(matrix12)
-- cr:set_source(pattern)
-- cr:paint()
-- testAngle=testAngle+step
-- if testAngle+step > end_angle - 0.05 then
-- testAngle = start_angle+0.05
-- level = level +1
-- if level > 2 then
-- break
-- end
-- end
-- end
end
local function repaint_layer(idx,content)
local lay = data.layers[idx]
if not lay then
data.layers[idx] = {}
lay = data.layers[idx]
lay.img = cairo.ImageSurface(cairo.Format.ARGB32, data.width, data.height)
lay.cr = cairo.Context(lay.img)
lay.cr:set_line_width(3)
end
local real_rad = data.angle or 0
if real_rad >= 0 then
real_rad = math.pi*2 - real_rad
else
real_rad = -real_rad
end
local count = #(lay.content or {})
local new_selected = count - math.floor( (real_rad)/(2*math.pi) * count )
if content or (lay.content and new_selected ~= lay.selected) then
lay.content = content or lay.content
lay.cr:set_operator(cairo.Operator.CLEAR)
lay.cr:paint()
lay.position = 0
lay.selected = new_selected
for k,v in ipairs(lay.content) do
gen_arc(idx,v)
end
lay.count = #lay.content
end
return lay.img
end
local function compose()
if not data.compose.img then
data.compose.img = cairo.ImageSurface(cairo.Format.ARGB32, data.width, data.height)
data.compose.cr = cairo.Context(data.compose.img)
else
data.compose.cr:set_operator(cairo.Operator.CLEAR)
data.compose.cr:paint()
data.compose.cr:set_operator(cairo.Operator.OVER)
end
local cr = data.compose.cr
for i=#data.layers,1,-1 do
cr:set_source_surface(repaint_layer(i),0,0)
cr:paint()
for k,v in ipairs(data.layers[i].content) do
local dr = (2*math.pi)/#data.layers[i].content
-- print("BLA BLA",k,i)
local r1 = dr*(k-1)
-- print(i,k,r1)
if i == 2 and k == 1 then
draw_text(cr,"1234567890123456789012345678901234567890123456789012345678901234567890",r1,r1+dr,i)
end
end
end
cr:set_source_surface(create_inner_circle(),0,0)
cr:paint()
cr:set_source_surface(position_indicator_layer(),0,0)
cr:paint()
end
function data:set_layer(idx,content)
if not data.w then
data.w =wibox({})
data.w.ontop = true
data.w.visible = true
data.w.width = data.width-- width
data.w.height = data.height
data.w.x = capi.mouse.coords().x - data.width/2
data.w.y = capi.mouse.coords().y - data.height/2
data.ib = data.ib or wibox.widget.imagebox()
data.w:set_widget(data.ib)
end
repaint_layer(idx,content)
end
data:set_layer(1,{
{name="test",icon="",func = function(menu,...) end },
{name="test",icon="",func = function(menu,...) end },
{name="test",icon="",func = function(menu,...) end },
{name="test",icon="",func = function(menu,...) end },
{name="test",icon="",func = function(menu,...) end },
})
data:set_layer(2,{
{name="test",icon="",func = function(menu,...) end },
{name="test",icon="",func = function(menu,...) end },
{name="test",icon="",func = function(menu,...) end },
{name="test",icon="",func = function(menu,...) end },
{name="test",icon="",func = function(menu,...) end },
{name="test",icon="",func = function(menu,...) end },
{name="test",icon="",func = function(menu,...) end },
{name="test",icon="",func = function(menu,...) end },
})
local focal = nil
capi.mousegrabber.run(function(mouse)
if not focal then
focal = {x= mouse.x,y=mouse.y}
end
if mouse.buttons[3] == true then
capi.mousegrabber.stop()
focal = nil
return false
end
local angle = math.atan2((mouse.y-focal.y),(mouse.x-focal.x))
data.tan = (mouse.y-focal.y)/(mouse.x-focal.x)
data.angle = angle
compose()
data.ib:set_image(data.compose.img)
data.w.shape_bounding = data.compose.img._native
return true
end,"fleur")
return data
end
local function get_direction(data)
return "left" -- Nothing to do
end
local function set_position(self)
return --Nothing to do
end
local function setup_drawable(data)
local internal = data._internal
local private_data = internal.private_data
--Init
-- internal.w = wibox({})
internal.margin = wibox.container.margin()
if not data.layout then
data.layout = layout.vertical
end
internal.layout = wibox.layout.fixed.horizontal() --data.layout(data) --TODO fix
internal.margin:set_widget(internal.layout)
--Getters
data.get_wibox = function() return nil end -- Will this break?
data.get_x = function() return 0 end
data.get_y = function() return 0 end
data.get_width = function() return 500 end
data.get_height = function() return 40 end
data.get_visible = function() return private_data.visible end
data.get_direction = function() return private_data.direction end
data.get_margins = function()
local ret = {left=data.border_width,right=data.border_width,top=data.style.margins.TOP,bottom=data.style.margins.BOTTOM}
if data.arrow_type ~= base.arrow_type.NONE then
ret[data.direction] = ret[data.direction]+13
end
return ret
end
--Setters
function internal:set_visible(value)
-- TODO
end
end
local function setup_item(data,item,args)
-- Add widgets
local tb = wibox.widget.textbox()
data._internal.layout:add(tb)
item.widget = tb
tb:set_text("bob")
end
local function new(args)
local args = args or {}
args.internal = args.internal or {}
args.internal.get_direction = args.internal.get_direction or get_direction
args.internal.set_position = args.internal.set_position or set_position
args.internal.setup_drawable = args.internal.setup_drawable or setup_drawable
args.internal.setup_item = args.internal.setup_item or setup_item
args.style = args.style or arrow_style
local ret = base(args)
ret:connect_signal("clear::menu",function(_,vis)
ret._internal.layout:reset()
end)
ret:connect_signal("_hidden::changed",function(_,item)
item.widget:emit_signal("widget::updated")
end)
return ret
end
return setmetatable(module, { __call = function(_, ...) return new(...) end })
-- kate: space-indent on; indent-width 2; replace-tabs on;