1---------------------------------------------------------------------------
2--- Module dedicated to gather common shape painters.
3--
4-- It add the concept of "shape" to Awesome. A shape can be applied to a
5-- background, a margin, a mask or a drawable shape bounding.
6--
7-- The functions exposed by this module always take a context as first
8-- parameter followed by the widget and height and additional parameters.
9--
10-- The functions provided by this module only create a path in the content.
11-- to actually draw the content, use `cr:fill()`, `cr:mask()`, `cr:clip()` or
12-- `cr:stroke()`
13--
14-- In many case, it is necessary to apply the shape using a transformation
15-- such as a rotation. The preferred way to do this is to wrap the function
16-- in another function calling `cr:rotate()` (or any other transformation
17-- matrix).
18--
19-- To specialize a shape where the API doesn't allows extra arguments to be
20-- passed, it is possible to wrap the shape function like:
21--
22--    local new_shape = function(cr, width, height)
23--        gears.shape.rounded_rect(cr, width, height, 2)
24--    end
25--
26-- Many elements can be shaped. This include:
27--
28-- * `client`s (see `gears.surface.apply_shape_bounding`)
29-- * `wibox`es (see `wibox.shape`)
30-- * All widgets (see `wibox.container.background`)
31-- * The progressbar (see `wibox.widget.progressbar.bar_shape`)
32-- * The graph (see `wibox.widget.graph.step_shape`)
33-- * The checkboxes (see `wibox.widget.checkbox.check_shape`)
34-- * Images (see `wibox.widget.imagebox.clip_shape`)
35-- * The taglist tags (see `awful.widget.taglist`)
36-- * The tasklist clients (see `awful.widget.tasklist`)
37-- * The tooltips (see `awful.tooltip`)
38--
39-- @author Emmanuel Lepage Vallee
40-- @copyright 2011-2016 Emmanuel Lepage Vallee
41-- @module gears.shape
42---------------------------------------------------------------------------
43local g_matrix = require( "gears.matrix" )
44local unpack   = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1)
45local atan2    = math.atan2 or math.atan -- lua 5.3 compat
46
47local module = {}
48
49--- Add a rounded rectangle to the current path.
50-- Note: If the radius is bigger than either half side, it will be reduced.
51--
52-- @DOC_gears_shape_rounded_rect_EXAMPLE@
53--
54-- @param cr A cairo content
55-- @tparam number width The rectangle width
56-- @tparam number height The rectangle height
57-- @tparam number radius the corner radius
58function module.rounded_rect(cr, width, height, radius)
59
60    radius = radius or 10
61
62    if width / 2 < radius then
63        radius = width / 2
64    end
65
66    if height / 2 < radius then
67        radius = height / 2
68    end
69
70    cr:move_to(0, radius)
71
72    cr:arc( radius      , radius       , radius,    math.pi   , 3*(math.pi/2) )
73    cr:arc( width-radius, radius       , radius, 3*(math.pi/2),    math.pi*2  )
74    cr:arc( width-radius, height-radius, radius,    math.pi*2 ,    math.pi/2  )
75    cr:arc( radius      , height-radius, radius,    math.pi/2 ,    math.pi    )
76
77    cr:close_path()
78end
79
80--- Add a rectangle delimited by 2 180 degree arcs to the path.
81--
82-- @DOC_gears_shape_rounded_bar_EXAMPLE@
83--
84-- @param cr A cairo content
85-- @param width The rectangle width
86-- @param height The rectangle height
87function module.rounded_bar(cr, width, height)
88    module.rounded_rect(cr, width, height, height / 2)
89end
90
91--- A rounded rect with only some of the corners rounded.
92--
93-- @DOC_gears_shape_partially_rounded_rect_EXAMPLE@
94--
95-- @param cr A cairo context
96-- @tparam number width The shape width
97-- @tparam number height The shape height
98-- @tparam boolean tl If the top left corner is rounded
99-- @tparam boolean tr If the top right corner is rounded
100-- @tparam boolean br If the bottom right corner is rounded
101-- @tparam boolean bl If the bottom left corner is rounded
102-- @tparam number rad The corner radius
103function module.partially_rounded_rect(cr, width, height, tl, tr, br, bl, rad)
104    rad = rad or 10
105    if width / 2 < rad then
106        rad = width / 2
107    end
108
109    if height / 2 < rad then
110        rad = height / 2
111    end
112
113    -- Top left
114    if tl then
115        cr:arc( rad, rad, rad, math.pi, 3*(math.pi/2))
116    else
117        cr:move_to(0,0)
118    end
119
120    -- Top right
121    if tr then
122        cr:arc( width-rad, rad, rad, 3*(math.pi/2), math.pi*2)
123    else
124        cr:line_to(width, 0)
125    end
126
127    -- Bottom right
128    if br then
129        cr:arc( width-rad, height-rad, rad, math.pi*2 , math.pi/2)
130    else
131        cr:line_to(width, height)
132    end
133
134    -- Bottom left
135    if bl then
136        cr:arc( rad, height-rad, rad, math.pi/2, math.pi)
137    else
138        cr:line_to(0, height)
139    end
140
141    cr:close_path()
142end
143
144--- A rounded rectangle with a triangle at the top.
145--
146-- @DOC_gears_shape_infobubble_EXAMPLE@
147--
148-- @param cr A cairo context
149-- @tparam number width The shape width
150-- @tparam number height The shape height
151-- @tparam[opt=5] number corner_radius The corner radius
152-- @tparam[opt=10] number arrow_size The width and height of the arrow
153-- @tparam[opt=width/2 - arrow_size/2] number arrow_position The position of the arrow
154function module.infobubble(cr, width, height, corner_radius, arrow_size, arrow_position)
155    arrow_size     = arrow_size     or 10
156    corner_radius  = math.min((height-arrow_size)/2, corner_radius or 5)
157    arrow_position = arrow_position or width/2 - arrow_size/2
158
159
160    cr:move_to(0 ,corner_radius+arrow_size)
161
162    -- Top left corner
163    cr:arc(corner_radius, corner_radius+arrow_size, (corner_radius), math.pi, 3*(math.pi/2))
164
165    -- The arrow triangle (still at the top)
166    cr:line_to(arrow_position                , arrow_size )
167    cr:line_to(arrow_position + arrow_size   , 0          )
168    cr:line_to(arrow_position + 2*arrow_size , arrow_size )
169
170    -- Complete the rounded rounded rectangle
171    cr:arc(width-corner_radius, corner_radius+arrow_size  , (corner_radius) , 3*(math.pi/2) , math.pi*2 )
172    cr:arc(width-corner_radius, height-(corner_radius)    , (corner_radius) , math.pi*2     , math.pi/2 )
173    cr:arc(corner_radius      , height-(corner_radius)    , (corner_radius) , math.pi/2     , math.pi   )
174
175    -- Close path
176    cr:close_path()
177end
178
179--- A rectangle terminated by an arrow.
180--
181-- @DOC_gears_shape_rectangular_tag_EXAMPLE@
182--
183-- @param cr A cairo context
184-- @tparam number width The shape width
185-- @tparam number height The shape height
186-- @tparam[opt=height/2] number arrow_length The length of the arrow part
187function module.rectangular_tag(cr, width, height, arrow_length)
188    arrow_length = arrow_length or height/2
189    if arrow_length > 0 then
190        cr:move_to(0            , height/2 )
191        cr:line_to(arrow_length , 0        )
192        cr:line_to(width        , 0        )
193        cr:line_to(width        , height   )
194        cr:line_to(arrow_length , height   )
195    else
196        cr:move_to(0            , 0        )
197        cr:line_to(-arrow_length, height/2 )
198        cr:line_to(0            , height   )
199        cr:line_to(width        , height   )
200        cr:line_to(width        , 0        )
201    end
202
203    cr:close_path()
204end
205
206--- A simple arrow shape.
207--
208-- @DOC_gears_shape_arrow_EXAMPLE@
209--
210-- @param cr A cairo context
211-- @tparam number width The shape width
212-- @tparam number height The shape height
213-- @tparam[opt=head_width] number head_width The width of the head (/\) of the arrow
214-- @tparam[opt=width /2] number shaft_width The width of the shaft of the arrow
215-- @tparam[opt=height/2] number shaft_length The head_length of the shaft (the rest is the head)
216function module.arrow(cr, width, height, head_width, shaft_width, shaft_length)
217    shaft_length = shaft_length or height / 2
218    shaft_width  = shaft_width  or width  / 2
219    head_width   = head_width   or width
220    local head_length  = height - shaft_length
221
222    cr:move_to    ( width/2                     , 0            )
223    cr:rel_line_to( head_width/2                , head_length  )
224    cr:rel_line_to( -(head_width-shaft_width)/2 , 0            )
225    cr:rel_line_to( 0                           , shaft_length )
226    cr:rel_line_to( -shaft_width                , 0            )
227    cr:rel_line_to( 0           , -shaft_length                )
228    cr:rel_line_to( -(head_width-shaft_width)/2 , 0            )
229
230    cr:close_path()
231end
232
233--- A squeezed hexagon filling the rectangle.
234--
235-- @DOC_gears_shape_hexagon_EXAMPLE@
236--
237-- @param cr A cairo context
238-- @tparam number width The shape width
239-- @tparam number height The shape height
240function module.hexagon(cr, width, height)
241    cr:move_to(height/2,0)
242    cr:line_to(width-height/2,0)
243    cr:line_to(width,height/2)
244    cr:line_to(width-height/2,height)
245    cr:line_to(height/2,height)
246    cr:line_to(0,height/2)
247    cr:line_to(height/2,0)
248    cr:close_path()
249end
250
251--- Double arrow popularized by the vim-powerline module.
252--
253-- @DOC_gears_shape_powerline_EXAMPLE@
254--
255-- @param cr A cairo context
256-- @tparam number width The shape width
257-- @tparam number height The shape height
258-- @tparam[opt=height/2] number arrow_depth The width of the arrow part of the shape
259function module.powerline(cr, width, height, arrow_depth)
260    arrow_depth = arrow_depth or height/2
261    local offset = 0
262
263    -- Avoid going out of the (potential) clip area
264    if arrow_depth < 0 then
265        width  =  width + 2*arrow_depth
266        offset = -arrow_depth
267    end
268
269    cr:move_to(offset                       , 0        )
270    cr:line_to(offset + width - arrow_depth , 0        )
271    cr:line_to(offset + width               , height/2 )
272    cr:line_to(offset + width - arrow_depth , height   )
273    cr:line_to(offset                       , height   )
274    cr:line_to(offset + arrow_depth         , height/2 )
275
276    cr:close_path()
277end
278
279--- An isosceles triangle.
280--
281-- @DOC_gears_shape_isosceles_triangle_EXAMPLE@
282--
283-- @param cr A cairo context
284-- @tparam number width The shape width
285-- @tparam number height The shape height
286function module.isosceles_triangle(cr, width, height)
287    cr:move_to( width/2, 0      )
288    cr:line_to( width  , height )
289    cr:line_to( 0      , height )
290    cr:close_path()
291end
292
293--- A cross (**+**) symbol.
294--
295-- @DOC_gears_shape_cross_EXAMPLE@
296--
297-- @param cr A cairo context
298-- @tparam number width The shape width
299-- @tparam number height The shape height
300-- @tparam[opt=width/3] number thickness The cross section thickness
301function module.cross(cr, width, height, thickness)
302    thickness = thickness or width/3
303    local xpadding   = (width  - thickness) / 2
304    local ypadding   = (height - thickness) / 2
305    cr:move_to(xpadding, 0)
306    cr:line_to(width - xpadding, 0)
307    cr:line_to(width - xpadding, ypadding)
308    cr:line_to(width           , ypadding)
309    cr:line_to(width           , height-ypadding)
310    cr:line_to(width - xpadding, height-ypadding)
311    cr:line_to(width - xpadding, height         )
312    cr:line_to(xpadding        , height         )
313    cr:line_to(xpadding        , height-ypadding)
314    cr:line_to(0               , height-ypadding)
315    cr:line_to(0               , ypadding       )
316    cr:line_to(xpadding        , ypadding       )
317    cr:close_path()
318end
319
320--- A similar shape to the `rounded_rect`, but with sharp corners.
321--
322-- @DOC_gears_shape_octogon_EXAMPLE@
323--
324-- @param cr A cairo context
325-- @tparam number width The shape width
326-- @tparam number height The shape height
327-- @tparam number corner_radius
328function module.octogon(cr, width, height, corner_radius)
329    corner_radius = corner_radius or math.min(10, math.min(width, height)/4)
330    local offset = math.sqrt( (corner_radius*corner_radius) / 2 )
331
332    cr:move_to(offset, 0)
333    cr:line_to(width-offset, 0)
334    cr:line_to(width, offset)
335    cr:line_to(width, height-offset)
336    cr:line_to(width-offset, height)
337    cr:line_to(offset, height)
338    cr:line_to(0, height-offset)
339    cr:line_to(0, offset)
340    cr:close_path()
341end
342
343--- A circle shape.
344--
345-- @DOC_gears_shape_circle_EXAMPLE@
346--
347-- @param cr A cairo context
348-- @tparam number width The shape width
349-- @tparam number height The shape height
350-- @tparam[opt=math.min(width  height) / 2)] number radius The radius
351function module.circle(cr, width, height, radius)
352    radius = radius or math.min(width, height) / 2
353    cr:move_to(width/2+radius, height/2)
354    cr:arc(width / 2, height / 2, radius, 0, 2*math.pi)
355    cr:close_path()
356end
357
358--- A simple rectangle.
359--
360-- @DOC_gears_shape_rectangle_EXAMPLE@
361--
362-- @param cr A cairo context
363-- @tparam number width The shape width
364-- @tparam number height The shape height
365function module.rectangle(cr, width, height)
366    cr:rectangle(0, 0, width, height)
367end
368
369--- A diagonal parallelogram with the bottom left corner at x=0 and top right
370-- at x=width.
371--
372-- @DOC_gears_shape_parallelogram_EXAMPLE@
373--
374-- @param cr A cairo context
375-- @tparam number width The shape width
376-- @tparam number height The shape height
377-- @tparam[opt=width/3] number base_width The parallelogram base width
378function module.parallelogram(cr, width, height, base_width)
379    base_width = base_width or width/3
380    cr:move_to(width-base_width, 0      )
381    cr:line_to(width           , 0      )
382    cr:line_to(base_width      , height )
383    cr:line_to(0               , height )
384    cr:close_path()
385end
386
387--- A losange.
388--
389-- @DOC_gears_shape_losange_EXAMPLE@
390--
391-- @param cr A cairo context
392-- @tparam number width The shape width
393-- @tparam number height The shape height
394function module.losange(cr, width, height)
395    cr:move_to(width/2 , 0        )
396    cr:line_to(width   , height/2 )
397    cr:line_to(width/2 , height   )
398    cr:line_to(0       , height/2 )
399    cr:close_path()
400end
401
402--- A pie.
403--
404-- The pie center is the center of the area.
405--
406-- @DOC_gears_shape_pie_EXAMPLE@
407--
408-- @param cr A cairo context
409-- @tparam number width The shape width
410-- @tparam number height The shape height
411-- @tparam[opt=0] number start_angle The start angle (in radian)
412-- @tparam[opt=math.pi/2] number end_angle The end angle (in radian)
413-- @tparam[opt=math.min(width height)/2] number radius The shape height
414function module.pie(cr, width, height, start_angle, end_angle, radius)
415    radius = radius or math.floor(math.min(width, height)/2)
416    start_angle, end_angle = start_angle or 0, end_angle or math.pi/2
417
418    -- If the shape is a circle, then avoid the lines
419    if math.abs(start_angle + end_angle - 2*math.pi) <= 0.01  then
420        cr:arc(width/2, height/2, radius, 0, 2*math.pi)
421    else
422        cr:move_to(width/2, height/2)
423        cr:line_to(
424            width/2 + math.cos(start_angle)*radius,
425            height/2 + math.sin(start_angle)*radius
426        )
427        cr:arc(width/2, height/2, radius, start_angle, end_angle)
428    end
429
430    cr:close_path()
431end
432
433--- A rounded arc.
434--
435-- The pie center is the center of the area.
436--
437-- @DOC_gears_shape_arc_EXAMPLE@
438--
439-- @param cr A cairo context
440-- @tparam number width The shape width
441-- @tparam number height The shape height
442-- @tparam[opt=math.min(width height)/2] number thickness The arc thickness
443-- @tparam[opt=0] number start_angle The start angle (in radian)
444-- @tparam[opt=math.pi/2] number end_angle The end angle (in radian)
445-- @tparam[opt=false] boolean start_rounded if the arc start rounded
446-- @tparam[opt=false] boolean end_rounded if the arc end rounded
447function module.arc(cr, width, height, thickness, start_angle, end_angle, start_rounded, end_rounded)
448    start_angle = start_angle or 0
449    end_angle   = end_angle   or math.pi/2
450
451    -- This shape is a partial circle
452    local radius = math.min(width, height)/2
453
454    thickness = thickness or radius/2
455
456    local inner_radius = radius - thickness
457
458    -- As the edge of the small arc need to touch the [start_p1, start_p2]
459    -- line, a small subset of the arc circumference has to be substracted
460    -- that's (less or more) equal to the thickness/2 (a little longer given
461    -- it is an arc and not a line, but it wont show)
462    local arc_percent = math.abs(end_angle-start_angle)/(2*math.pi)
463    local arc_length  = ((radius-thickness/2)*2*math.pi)*arc_percent
464
465    if start_rounded then
466        arc_length = arc_length - thickness/2
467
468        -- And back to angles
469        start_angle = end_angle - (arc_length/(radius - thickness/2))
470    end
471
472    if end_rounded then
473        arc_length = arc_length - thickness/2
474
475        -- And back to angles. Also make sure to avoid underflowing when the
476        -- rounded edge radius is greater than the angle delta.
477        end_angle = start_angle + math.max(
478            0, arc_length/(radius - thickness/2)
479        )
480    end
481
482    -- The path is a curcular arc joining 4 points
483
484    -- Outer first corner
485    local start_p1 = {
486        width /2 + math.cos(start_angle)*radius,
487        height/2 + math.sin(start_angle)*radius
488    }
489
490    if start_rounded then
491
492        -- Inner first corner
493        local start_p2 = {
494            width /2 + math.cos(start_angle)*inner_radius,
495            height/2 + math.sin(start_angle)*inner_radius
496        }
497
498        local median_angle = atan2(
499              start_p2[1] - start_p1[1],
500            -(start_p2[2] - start_p1[2])
501        )
502
503        local arc_center = {
504            (start_p1[1] + start_p2[1])/2,
505            (start_p1[2] + start_p2[2])/2,
506        }
507
508        cr:arc(arc_center[1], arc_center[2], thickness/2,
509            median_angle-math.pi/2, median_angle+math.pi/2
510        )
511
512    else
513        cr:move_to(unpack(start_p1))
514    end
515
516    cr:arc(width/2, height/2, radius, start_angle, end_angle)
517
518    if end_rounded then
519
520        -- Outer second corner
521        local end_p1 = {
522            width /2 + math.cos(end_angle)*radius,
523            height/2 + math.sin(end_angle)*radius
524        }
525
526        -- Inner first corner
527        local end_p2 = {
528            width /2 + math.cos(end_angle)*inner_radius,
529            height/2 + math.sin(end_angle)*inner_radius
530        }
531        local median_angle = atan2(
532              end_p2[1] - end_p1[1],
533            -(end_p2[2] - end_p1[2])
534        ) - math.pi
535
536        local arc_center = {
537            (end_p1[1] + end_p2[1])/2,
538            (end_p1[2] + end_p2[2])/2,
539        }
540
541        cr:arc(arc_center[1], arc_center[2], thickness/2,
542            median_angle-math.pi/2, median_angle+math.pi/2
543        )
544
545    end
546
547    cr:arc_negative(width/2, height/2, inner_radius, end_angle, start_angle)
548
549    cr:close_path()
550end
551
552--- A partial rounded bar. How much of the rounded bar is visible depends on
553-- the given percentage value.
554--
555-- Note that this shape is not closed and thus filling it doesn't make much
556-- sense.
557--
558-- @DOC_gears_shape_radial_progress_EXAMPLE@
559--
560-- @param cr A cairo context
561-- @tparam number w The shape width
562-- @tparam number h The shape height
563-- @tparam number percent The progressbar percent
564-- @tparam boolean hide_left Do not draw the left side of the shape
565function module.radial_progress(cr, w, h, percent, hide_left)
566    percent = percent or 1
567    local total_length = (2*(w-h))+2*((h/2)*math.pi)
568    local bar_percent  = (w-h)/total_length
569    local arc_percent  = ((h/2)*math.pi)/total_length
570
571    -- Bottom line
572    if percent > bar_percent then
573        cr:move_to(h/2,h)
574        cr:line_to((h/2) + (w-h),h)
575        cr:stroke()
576    elseif percent < bar_percent then
577        cr:move_to(h/2,h)
578        cr:line_to(h/2+(total_length*percent),h)
579        cr:stroke()
580    end
581
582    -- Right arc
583    if percent >= bar_percent+arc_percent then
584        cr:arc(w-h/2 , h/2, h/2,3*(math.pi/2),math.pi/2)
585        cr:stroke()
586    elseif percent > bar_percent and percent < bar_percent+(arc_percent/2) then
587        cr:arc(w-h/2 , h/2, h/2,(math.pi/2)-((math.pi/2)*((percent-bar_percent)/(arc_percent/2))),math.pi/2)
588        cr:stroke()
589    elseif percent >= bar_percent+arc_percent/2 and percent < bar_percent+arc_percent then
590        cr:arc(w-h/2 , h/2, h/2,0,math.pi/2)
591        cr:stroke()
592        local add = (math.pi/2)*((percent-bar_percent-arc_percent/2)/(arc_percent/2))
593        cr:arc(w-h/2 , h/2, h/2,2*math.pi-add,0)
594        cr:stroke()
595    end
596
597    -- Top line
598    if percent > 2*bar_percent+arc_percent then
599        cr:move_to((h/2) + (w-h),0)
600        cr:line_to(h/2,0)
601        cr:stroke()
602    elseif percent > bar_percent+arc_percent and percent < 2*bar_percent+arc_percent then
603        cr:move_to((h/2) + (w-h),0)
604        cr:line_to(((h/2) + (w-h))-total_length*(percent-bar_percent-arc_percent),0)
605        cr:stroke()
606    end
607
608    -- Left arc
609    if not hide_left then
610        if percent > 0.985 then
611            cr:arc(h/2, h/2, h/2,math.pi/2,3*(math.pi/2))
612            cr:stroke()
613        elseif percent  > 2*bar_percent+arc_percent then
614            local relpercent = (percent - 2*bar_percent - arc_percent)/arc_percent
615            cr:arc(h/2, h/2, h/2,3*(math.pi/2)-(math.pi)*relpercent,3*(math.pi/2))
616            cr:stroke()
617        end
618    end
619end
620
621--- Adjust the shape using a transformation object
622--
623-- Apply various transformations to the shape
624--
625-- @usage gears.shape.transform(gears.shape.rounded_bar)
626--    : rotate(math.pi/2)
627--       : translate(10, 10)
628--
629-- @param shape A shape function
630-- @return A transformation handle, also act as a shape function
631function module.transform(shape)
632
633    -- Apply the transformation matrix and apply the shape, then restore
634    local function apply(self, cr, width, height, ...)
635        cr:save()
636        cr:transform(self.matrix:to_cairo_matrix())
637        shape(cr, width, height, ...)
638        cr:restore()
639    end
640    -- Redirect function calls like :rotate() to the underlying matrix
641    local function index(_, key)
642        return function(self, ...)
643            self.matrix = self.matrix[key](self.matrix, ...)
644            return self
645        end
646    end
647
648    local result = setmetatable({
649        matrix = g_matrix.identity
650    }, {
651        __call = apply,
652        __index = index
653    })
654
655    return result
656end
657
658return module
659
660-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80
661