circles.lua

-- Coypright 2015-2017 Stefan Göbel.
--
-- This file is part of Corky.
--
-- Corky is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
-- License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
-- later version.
--
-- Corky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
-- warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
-- details.
--
-- You should have received a copy of the GNU General Public License along with Corky. If not, see
-- <http://www.gnu.org/licenses/>.

-------------------------------------------------------------------------------------------------------------------

--- Circular meters for *Corky*.
--
--  **Loading the module:**
--
--    #: include, corky.circles
--
--  **Adding a circular meter:**
--
--  _**Note:** This is only one line in the actual configuration. Do not add the line break!_
--
--    #: circle, <name>, <value>, <x>, <y>, <radius>, <width>, <start>, <angle>, <fcol>
--       [, <bcol>[, <scale>[, <scol>[, <min>[, <max>[, <hook>]]]]]]
--
--  **Parameters:**
--
--  *`<name>`*
--
--  * The name of the meter, may be any arbitrary string. It is currently not used and may be left empty. Multiple
--  meters with the same name are allowed.
--
--  *`<value>`*
--
--  * The value to display, e.g. `${cpu cpu1}` for a circular meter showing the current CPU usage of the first CPU.
--  The value will be retrieved using the corky.cache module's percent() function. To get
--  correct values, it is necessary to manually add the value to the cache and define its minimum and maximum value
--  (either in a Lua script using set() or using the `cache` directive in the configuration
--  file), even if the value itself is already a percentage (like `${cpu cpu1}`)! Please see the corky.cache
--  module for more details.
--
--  *`<x>`*
--
--  * The X coordinate of the circle's center. Must be a valid integer greater than or equal to `0`.
--
--  *`<y>`*
--
--  * The Y coordinate of the circle's center. Must be a valid integer greater than or equal to `0`.
--
--  *`<radius>`*
--
--  * The radius of the circle. Must be a valid integer greater than `0`.
--
--  *`<width>`*
--
--  * The width of the meter. Must be a valid integer greater than `0`.
--
--  *`<start>`*
--
--  * The start angle: the angle where a value of 0% (or the `<min>` value) is located, in degrees. Must be a valid
--  number greater than or equal to `0` and less than `360`. An angle of `0` is in the direction of the positive X
--  axis, the angle increases in clockwise direction.
--
--  *`<angle>`*
--
--  * The total angle of the meter, in degrees. This must be a valid number between `-360` and `360` (inclusive).
--  It must not be `0`. The meter will be drawn in clockwise direction for positive values, and counter-clockwise
--  for negative values.
--
--  *`<fcol>`*
--
--  * The foreground color, i.e. the color used to draw the meter from 0% (or the `<min>` value) to the value. The
--  corky.dcolors module will be used to resolve the color, so any named color (see corky.colors), fixed
--  dynamic color or gradient color (see corky.dcolors) may be used, or a `0x<AARRGGBB>` value (or in fact any
--  valid number between `0` and `0xffffffff`, see the colors module) may be used directly.
--
--  * **Note:** There will be no gradient displayed on the meter, i.e. the foreground color will be one color
--  across the length of the meter. The gradient (or fixed dynamic color), if one is used, will just be used to
--  calculate this one color based on the value. If dynamic colors are used, the threshold values must be specified
--  in percent. See corky.dcolors for more details.
--
--  *`<bcol>`*
--
--  * Optional. The background color. If specified, the part of the meter between the value and the 100% position
--  (or the `<max>` value) will be filled with this color. The same notes as for the foreground color apply, please
--  see above.
--
--  *`<scale>`*
--
--  * Optional. The name of a list of scale positions. See the corky.lists module on how to define a list. If
--  this is specified, the list must contain an arbitrary number of numeric values between `0` and `100`
--  (inclusive), in ascending order. These values specify the positions (in percent) of scale marks to be drawn
--  over the meter. To enable these marks, the parameter `<scol>` has to be specified, too.
--
--  *`<scol>`*
--
--  * Optional. The color for the scale marks. The same notes as for the foreground color apply, see above. Note
--  that the `<scale>` has to be a valid list as described above for marks to be drawn.
--
--  *`<min>`*
--
--  * Optional. The minimum value in percent, defaults to `0` if not specified.
--
--  *`<max>`*
--
--  * Optional. The maximum value in percent, defaults to `100` if not specified.
--
--  *`<hook>`*
--
--  * Optional. Specifies when to draw the circle. It must be either `"pre"` (to draw it during the pre-draw hook)
--  or `"post"` (to draw it during the post-draw hook). The default value is "post".
--
--  **Examples:**
--
--  Display the (total) CPU usage. Colors are defined using the `color` (see corky.colors) and `dcolor` (see
--  corky.dcolors) directives, and scale marks will be shown in 10% steps.
--
--  Note that optional values at the end may be left out completely, while the optional name of the meter may be
--  left empty, but the (empty) field itself has to be included!
--
--    #: cache, ${cpu}, 1, 0, 100
--
--    #: color,  black, 0x000000
--    #: color,  grey,  0x444444
--    #: dcolor, fg,    0x00ff00, 50, 0xffff00, 0xff0000
--
--    #: list, scale, 0, 10, 20, 30, 40, 50, 60, 70, 80, 90
--
--    #: circle,, ${cpu}, 100, 100, 50, 10, 270, +360, fg, grey, scale, black
--
--  Display the (total) CPU usage using two circular meters:
--
--    #: cache, ${cpu}, 1, 0, 100
--
--    #: circle,, ${cpu}, 70, 250, 50, 10, 90, +180, 0xff0000, 0x444444,,,  0,  50
--    #: circle,, ${cpu}, 70, 250, 37, 10, 90, +180, 0xff0000, 0x444444,,, 50, 100
--
--  This example will display two circular meters for the CPU usage. Values between 0% and 50% will be shown in the
--  first meter, i.e. if the CPU usage is 25%, the first meter will display a 50% value, while the second meter
--  will show 0%. Values between 50% and 100% will be shown in the second meter, i.e. for a CPU usage of 75% the
--  first meter will show a 100% value, and the second meter will show a 50% value.
--
--  If you want to use scale marks on the meters, note that these are defined *per meter*. If in the example above
--  a list containing a single value of `50` is specified as `<scale>` parameter, a scale mark at the 50% position
--  of *both* meters will be drawn (i.e. at the 25% and 75% CPU usage positions)!
--
--  @MO corky.circles
--  @CO © 2015-2017 Stefan Göbel
--  @RE 2017033001
--  @LI [GPLv3](http://www.gnu.org/copyleft/gpl.html)
--  @AU Stefan Göbel [[⌂]](http://subtype.de/) [[✉]](mailto:corky@subtype.de)
--  @AL this

-------------------------------------------------------------------------------------------------------------------

--- Stores the defined meters.
--
--  The keys are the meter names, the values are arrays containing tables with the meter data defined under that
--  name, in the order they are added. The meter data tables contain the following keys: `value`, `x`, `y`,
--  `radius`, `width`, `start`, `angle`, `fcol`, `bcol`, `scale`, `scol`, `min`, `max`, `hook`. The scale will
--  already be resolved to the appropriate list, all colors will be strings (or numbers) and will be resolved at
--  runtime to allow for dynamic colors.
--
--  @l circles

-------------------------------------------------------------------------------------------------------------------

local utils   = require "corky.utils"                                         -- For table.pack() and .unpack().
local cache   = require "corky.cache"
local cairo   = require "corky.cairo"
local colors  = require "corky.dcolors"
local lists   = require "corky.lists"
local v       = require "corky.validator"
local circles = {}
local this    = {}

-------------------------------------------------------------------------------------------------------------------

--- Add a circular meter.
--
--  The parameters are the same as the parameters for the configuration directive, in the same order. Please see
--  above for more details.
--
--  This function will add the new meter to the list of meters, it will not draw it!
--
--  @p ...
--  * `name`: (*string*) Name of the meter.
--  * `value`: (*string*) Value of the meter.
--  * `x`: (**_int_**) X coordinate of the center (`x >= 0`).
--  * `y`: (**_int_**) Y coordinate of the center (`y >= 0`).
--  * `radius`: (**_int_**) The radius of the circle (`radius > 0`).
--  * `width`: (**_int_**) The width of the meter (`width > 0`).
--  * `start`: (**_number_**) The start angle, in degrees (`0 <= start < 360`).
--  * `angle`: (**_number_**) The total angle, in degrees (`-360 <= angle <= 360` and `angle ~= 0`).
--  * `fcol`: The foreground color, uses corky.dcolors to get the actual color.
--  * `bcol`: The background color, uses corky.dcolors to get the actual color. (*optional*)
--  * `scale`: (*string*) Name of a list with scale mark positions, see corky.lists. (*optional*)
--  * `scol`: Scale color, uses corky.dcolors to get the actual color. (*optional*)
--  * `min`: (**_number_**) Minimum value for the meter (`0 <= min < 100`). (*optional*)
--  * `max`: (**_number_**) Maximum value for the meter (`0 < max <= 100`). (*optional*)
--  * `hook`: (*string*) When to draw the meter (either `"pre"` or `"post"`). (*optional*)
--  @B Returns `true` if the meter could be added, `false` in case of any error.

this.add = function (name, value, x, y, radius, width, start, angle, fcol, bcol, scale, scol, min, max, hook)

   local valid = true

   valid, name   = v { v (name,   valid) . en . dv ("")                            }
   valid, value  = v { v (value,  valid) . r . s . ne ("")                         }
   valid, x      = v { v (x,      valid) . n . ge (   0)                           }
   valid, y      = v { v (y,      valid) . n . ge (   0)                           }
   valid, radius = v { v (radius, valid) . n . gt (   0)                           }
   valid, width  = v { v (width,  valid) . n . gt (   0)                           }
   valid, start  = v { v (start,  valid) . n . ge (   0) . lt (360)                }
   valid, angle  = v { v (angle,  valid) . n . ge (-360) . le (360) . ne (0)       }
   valid, fcol   = v { v (fcol,   valid) . en . r                                  }
   valid, bcol   = v { v (bcol,   valid) . en                                      }
   valid, scale  = v { v (scale,  valid) . en                                      }
   valid, scol   = v { v (scol,   valid) . en                                      }
   valid, min    = v { v (min,    valid) . en . dv (  0) . n . ge (  0) . lt (100) }
   valid, max    = v { v (max,    valid) . en . dv (100) . n . gt (min) . le (100) }
   valid, hook   = v { v (hook,   valid) . en . dv ("post") . eq ("pre", "post")   }

   if not valid then
      return false
   end

   if scale then
      scale = lists.get (scale)
      if not scale then
         return false
      end
      local last = nil
      for i = 1, #scale do
         valid, scale [i] =  v { v (scale [i]) . n . ir (0, 100) . ni (last) . gt (last) }
         if not valid then
            return false
         end
         last = scale [i]
      end
   end

   local circle = {
      value  = value,
      x      = x,
      y      = y,
      radius = radius,
      width  = width,
      start  = start,
      angle  = angle,
      fcol   = fcol,
      bcol   = bcol,
      scale  = scale,
      scol   = scol,
      min    = min,
      max    = max,
      hook   = hook
   }

   if circles [name] then
      circles [name] [#(circles [name]) + 1] = circle
   else
      circles [name] = { circle }
   end

   return true

end

-------------------------------------------------------------------------------------------------------------------

--- Draw a circular meter.
--
--  **Note:** This function assumes that corky.cairo.init() has been called and a valid surface and
--  context are available!
--
--  @i x X coordinate of the circle's center.
--  @i y Y coordinate of the circle's center.
--  @i radius The radius of the circle.
--  @i width The width of the meter.
--  @n start The start angle, in degrees.
--  @n angle The total angle, in degrees.
--  @n p The percent value, relative to the meter.
--  @t fcol The foreground color, as `{ r, g, b, a }`, all values between `0` and `1` (inclusive).
--  @t[opt] bcol The background color, as `{ r, g, b, a }`, all values between `0` and `1` (inclusive).
--  @t[opt] scale Array containing the scale mark positions, in percent.
--  @t[opt] scol The scale marks' color, as `{ r, g, b, a }`, all values between `0` and `1` (inclusive).

this.arc = function (x, y, radius, width, start, angle, p, fcol, bcol, scale, scol)

   local arc_func = cairo.arc
   if angle < 0 then
      arc_func = cairo.arc_negative
   end

   local stop = start + angle

   if angle > 0 then
      while start >= stop do
         start = start - 360
      end
   else
      while start <= stop do
         start = start + 360
      end
   end

   local start_angle = start / 180 * math.pi
   local stop_angle  = stop  / 180 * math.pi
   local value_angle = p     / 100 * (stop_angle - start_angle) + start_angle

   cairo.set_line_width  (cairo.c, width)

   if bcol and p ~= 100 then
      cairo.set_source_rgba (cairo.c, table.unpack (bcol))
      arc_func              (cairo.c, x, y, radius, value_angle, stop_angle)
      cairo.stroke          (cairo.c)
   end

   if p ~= 0 then
      cairo.set_source_rgba (cairo.c, table.unpack (fcol))
      arc_func              (cairo.c, x, y, radius, start_angle, value_angle)
      cairo.stroke          (cairo.c)
   end

   if scale and scol then

      cairo.set_source_rgba (cairo.c, table.unpack (scol))
      cairo.set_line_width  (cairo.c, 1)

      for i, pos in ipairs (scale) do

         local a = pos / 100 * (stop_angle - start_angle) + start_angle + 0.5 * math.pi

         local dx = 0 + (radius + width / 2) * math.sin (a)
         local dy = 0 - (radius + width / 2) * math.cos (a)

         cairo.move_to (cairo.c, x + dx, y + dy)

         dx = 0 + (radius - width / 2) * math.sin (a)
         dy = 0 - (radius - width / 2) * math.cos (a)

         cairo.line_to (cairo.c, x + dx, y + dy)

         cairo.stroke (cairo.c)

      end

   end

end

-------------------------------------------------------------------------------------------------------------------

do

   --- Draw the meters.
   --
   --  @s hook Which meters to draw, either `"pre"` or `"post"`.

   local function draw (hook)

      for _, entry in pairs (circles) do
         for i = 1, #entry do
            local c = entry [i]
            if c.hook == hook then
               local valid, p, fcol, bcol, scol = true, cache.percent (c.value)
               if p then
                  valid, fcol = v { v (c.fcol, valid) . r . ch (colors.get (c.fcol, p)) . r }
                  valid, bcol = v { v (c.bcol, valid) . o . ch (colors.get (c.bcol, p)) . r }
                  valid, scol = v { v (c.scol, valid) . o . ch (colors.get (c.scol, p)) . r }
                  if valid then
                     if c.min ~= 0 or c.max ~= 100 then
                        p = utils.percent (p, c.min, c.max)
                     end
                     this.arc (c.x, c.y, c.radius, c.width, c.start, c.angle, p, fcol, bcol, c.scale, scol)
                  end
               end
            end
         end
      end

   end

   ----------------------------------------------------------------------------------------------------------------

   --- Configuration handler for circle settings.
   --
   --  This handler will be called automatically for every `circle` directive in the configuration
   --  file.
   --
   --  @t setting Array containing the configuration directive split into its individual parts.
   --  @B If successful `true`, `false` in case of any error.
   --  @c corky

   local function config_handler (setting)

      if #setting < 10 or #setting > 16 then                                     -- Wrong number of parameters.
         return false
      end

      return this.add (select (2, table.unpack (setting)))

   end

   local config = require "corky.config"
   config.handler ("circle", config_handler)

   local hooks = require "corky.hooks"
   hooks.add ("pre_draw",  function () draw ("pre" ) end)
   hooks.add ("post_draw", function () draw ("post") end)

end

-------------------------------------------------------------------------------------------------------------------

return this

--                                                     :indentSize=3:tabSize=3:noTabs=true:mode=lua:maxLineLen=115:
generated by LDoc 1.4.6 Last updated 2019-02-14 20:51:07