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