From f6e3ff9574e3417647e23b841d133684ef29a320 Mon Sep 17 00:00:00 2001 From: ndwarshuis Date: Wed, 20 Jul 2022 00:11:03 -0400 Subject: [PATCH] REF remove submodule for core and move all lua files to common subdir --- .gitmodules | 3 - config/schema.yml | 3 + conky.conf | 28 +- core | 1 - schema/geometry.lua | 20 -- schema/theme.lua | 64 ---- src/color.lua | 87 +++++ {drawing => src}/compile.lua | 0 src/err.lua | 52 +++ src/format.lua | 43 +++ src/i_o.lua | 107 ++++++ src/impure.lua | 28 ++ src/json.lua | 194 ++++++++++ {drawing => src/modules}/common.lua | 35 +- {drawing => src/modules}/filesystem.lua | 3 +- {drawing => src/modules}/graphics.lua | 7 +- {drawing => src/modules}/memory.lua | 0 {drawing => src/modules}/network.lua | 0 {drawing => src/modules}/pacman.lua | 0 {drawing => src/modules}/power.lua | 0 {drawing => src/modules}/processor.lua | 10 +- {drawing => src/modules}/readwrite.lua | 4 + {drawing => src/modules}/system.lua | 0 src/pure.lua | 259 ++++++++++++++ src/sys.lua | 332 ++++++++++++++++++ src/widget/arc/arc.lua | 29 ++ src/widget/arc/circle.lua | 55 +++ src/widget/arc/compound_dial.lua | 60 ++++ src/widget/arc/dial.lua | 75 ++++ src/widget/dynamic.lua | 45 +++ src/widget/geom.lua | 60 ++++ src/widget/image/Image.lua | 31 ++ src/widget/image/ScaledImage.lua | 31 ++ src/widget/line/bar.lua | 61 ++++ src/widget/line/compound_bar.lua | 67 ++++ src/widget/line/line.lua | 74 ++++ src/widget/path.lua | 73 ++++ src/widget/rect/fill_rect.lua | 34 ++ src/widget/rect/rect.lua | 40 +++ src/widget/shape.lua | 70 ++++ src/widget/source.lua | 117 ++++++ src/widget/style.lua | 55 +++ src/widget/text/text.lua | 58 +++ src/widget/text/text_column.lua | 49 +++ src/widget/text/text_internal.lua | 165 +++++++++ src/widget/text/text_table.lua | 183 ++++++++++ src/widget/text/text_threshold.lua | 79 +++++ src/widget/timeseries/scaled_timeseries.lua | 269 ++++++++++++++ src/widget/timeseries/timeseries.lua | 163 +++++++++ src/widget/timeseries/timeseries_internal.lua | 132 +++++++ src/widget/timeseries/xlabels.lua | 46 +++ src/widget/timeseries/ylabels.lua | 47 +++ 52 files changed, 3300 insertions(+), 148 deletions(-) delete mode 100644 .gitmodules delete mode 160000 core delete mode 100644 schema/geometry.lua delete mode 100644 schema/theme.lua create mode 100644 src/color.lua rename {drawing => src}/compile.lua (100%) create mode 100644 src/err.lua create mode 100644 src/format.lua create mode 100644 src/i_o.lua create mode 100644 src/impure.lua create mode 100644 src/json.lua rename {drawing => src/modules}/common.lua (96%) rename {drawing => src/modules}/filesystem.lua (96%) rename {drawing => src/modules}/graphics.lua (97%) rename {drawing => src/modules}/memory.lua (100%) rename {drawing => src/modules}/network.lua (100%) rename {drawing => src/modules}/pacman.lua (100%) rename {drawing => src/modules}/power.lua (100%) rename {drawing => src/modules}/processor.lua (97%) rename {drawing => src/modules}/readwrite.lua (94%) rename {drawing => src/modules}/system.lua (100%) create mode 100644 src/pure.lua create mode 100644 src/sys.lua create mode 100644 src/widget/arc/arc.lua create mode 100644 src/widget/arc/circle.lua create mode 100644 src/widget/arc/compound_dial.lua create mode 100644 src/widget/arc/dial.lua create mode 100644 src/widget/dynamic.lua create mode 100644 src/widget/geom.lua create mode 100644 src/widget/image/Image.lua create mode 100644 src/widget/image/ScaledImage.lua create mode 100644 src/widget/line/bar.lua create mode 100644 src/widget/line/compound_bar.lua create mode 100644 src/widget/line/line.lua create mode 100644 src/widget/path.lua create mode 100644 src/widget/rect/fill_rect.lua create mode 100644 src/widget/rect/rect.lua create mode 100644 src/widget/shape.lua create mode 100644 src/widget/source.lua create mode 100644 src/widget/style.lua create mode 100644 src/widget/text/text.lua create mode 100644 src/widget/text/text_column.lua create mode 100644 src/widget/text/text_internal.lua create mode 100644 src/widget/text/text_table.lua create mode 100644 src/widget/text/text_threshold.lua create mode 100644 src/widget/timeseries/scaled_timeseries.lua create mode 100644 src/widget/timeseries/timeseries.lua create mode 100644 src/widget/timeseries/timeseries_internal.lua create mode 100644 src/widget/timeseries/xlabels.lua create mode 100644 src/widget/timeseries/ylabels.lua diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 4af362e..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "core"] - path = core - url = gitolite:conky/core.git diff --git a/config/schema.yml b/config/schema.yml index 76c64c0..4c2d07a 100644 --- a/config/schema.yml +++ b/config/schema.yml @@ -49,6 +49,9 @@ properties: required: [show_temp, show_clock, show_gpu_util, show_mem_util, show_vid_util] additionalProperties: false properties: + dev_power: + description: the sysfs path to the graphics card power indicator + type: string show_temp: description: show the GPU temp type: boolean diff --git a/conky.conf b/conky.conf index df13e26..c34131c 100644 --- a/conky.conf +++ b/conky.conf @@ -3,16 +3,14 @@ local conky_dir = debug.getinfo(1).source:match("@?(.*/)") local subdirs = { - '?.lua', - 'drawing/?.lua', - 'schema/?.lua', - 'core/?.lua', - 'core/widget/?.lua', - 'core/widget/arc/?.lua', - 'core/widget/text/?.lua', - 'core/widget/timeseries/?.lua', - 'core/widget/rect/?.lua', - 'core/widget/line/?.lua', + 'src/?.lua', + 'src/modules/?.lua', + 'src/widget/?.lua', + 'src/widget/arc/?.lua', + 'src/widget/text/?.lua', + 'src/widget/timeseries/?.lua', + 'src/widget/rect/?.lua', + 'src/widget/line/?.lua', 'lib/share/lua/5.4/?.lua', 'lib/share/lua/5.4/?/init.lua', } @@ -41,7 +39,7 @@ if i_o.exe_exists('yajsv') then end else validate_config = function(_) - print('WARNING: could not validate config') + i_o.warnf('could not validate config') return true end end @@ -52,16 +50,16 @@ local find_valid_config = function(paths) local r = i_o.read_file(path) if r ~= nil then if validate_config(path) then - i_o.printf('INFO: Using config at %s', path) + i_o.infof('Using config at %s', path) return path, yaml.load(r) else - i_o.printf('WARNING: %s did not pass; trying next', path) + i_o.warnf('%s did not pass; trying next', path) end else - i_o.printf('INFO: could not find %s; trying next', path) + i_o.infof('could not find %s; trying next', path) end end - assert(false, 'ERROR: could not load valid config') + i_o.assertf(false, 'ERROR: could not load valid config') end local get_config_dir = function() diff --git a/core b/core deleted file mode 160000 index ac26047..0000000 --- a/core +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ac2604709f5344125519913849e9013e1dfea717 diff --git a/schema/geometry.lua b/schema/geometry.lua deleted file mode 100644 index ee90afb..0000000 --- a/schema/geometry.lua +++ /dev/null @@ -1,20 +0,0 @@ -local M = {} - -M.LEFT_X = 32 -M.SECTION_WIDTH = 436 -M.CENTER_PAD = 20 -M.PANEL_HORZ_SPACING = 10 -M.PANEL_MARGIN_X = 20 -M.PANEL_MARGIN_Y = 10 -M.TOP_Y = 21 -M.SIDE_HEIGHT = 1020 -M.CENTER_HEIGHT = 220 - -local margin_width = M.PANEL_MARGIN_X * 2 + M.PANEL_HORZ_SPACING - -M.CENTER_LEFT_X = M.LEFT_X + M.SECTION_WIDTH + margin_width -M.CENTER_RIGHT_X = M.CENTER_LEFT_X + M.SECTION_WIDTH + M.CENTER_PAD -M.CENTER_WIDTH = M.SECTION_WIDTH * 2 + M.CENTER_PAD -M.RIGHT_X = M.CENTER_LEFT_X + M.CENTER_WIDTH + margin_width - -return M diff --git a/schema/theme.lua b/schema/theme.lua deleted file mode 100644 index 32c17fe..0000000 --- a/schema/theme.lua +++ /dev/null @@ -1,64 +0,0 @@ -local M = {} - -local color = require 'color' - -M.FONT = 'Neuropolitical' - --- text colors -M.HEADER_FG = color.rgb(0xefefef) - -M.PRIMARY_FG = color.rgb(0xbfe1ff) -M.CRITICAL_FG = color.rgb(0xff8282) - -M.INACTIVE_TEXT_FG = color.rgb(0xc8c8c8) -M.MID_GREY = color.rgb(0xd6d6d6) -M.BORDER_FG = color.rgb(0x888888) -M.PLOT_GRID_FG = color.rgb(0x666666) -M.PLOT_OUTLINE_FG = color.rgb(0x777777) - - --- arc bg colors -local GREY2 = 0xbfbfbf -local GREY5 = 0x565656 -M.INDICATOR_BG = color.gradient_rgb{ - [0.0] = GREY5, - [0.5] = GREY2, - [1.0] = GREY5 -} - --- arc/bar fg colors -local PRIMARY1 = 0x99CEFF -local PRIMARY3 = 0x316BA6 -M.INDICATOR_FG_PRIMARY = color.gradient_rgb{ - [0.0] = PRIMARY3, - [0.5] = PRIMARY1, - [1.0] = PRIMARY3 -} - -local CRITICAL1 = 0xFF3333 -local CRITICAL3 = 0xFFB8B8 -M.INDICATOR_FG_CRITICAL = color.gradient_rgb{ - [0.0] = CRITICAL1, - [0.5] = CRITICAL3, - [1.0] = CRITICAL1 -} - --- plot colors -local PLOT_PRIMARY1 = 0x003f7c -local PLOT_PRIMARY2 = 0x1e90ff -local PLOT_PRIMARY3 = 0x316ece -local PLOT_PRIMARY4 = 0x8cc7ff -M.PLOT_FILL_BORDER_PRIMARY = color.gradient_rgb{ - [0.0] = PLOT_PRIMARY1, - [1.0] = PLOT_PRIMARY2 -} - -M.PLOT_FILL_BG_PRIMARY = color.gradient_rgba{ - [0.2] = {PLOT_PRIMARY3, 0.5}, - [1.0] = {PLOT_PRIMARY4, 1.0} -} - --- panel pattern -M.PANEL_BG = color.rgba(0x121212, 0.7) - -return M diff --git a/src/color.lua b/src/color.lua new file mode 100644 index 0000000..bedbded --- /dev/null +++ b/src/color.lua @@ -0,0 +1,87 @@ +local err = require 'err' + +-------------------------------------------------------------------------------- +-- colors +-- +-- these are tables like {red :: Int, green :: Int, blue :: Int, alpha :: Int} + +local rgba = function(hex, alpha) + local obj = err.safe_table( + { + r = ((hex / 0x10000) % 0x100) / 255., + g = ((hex / 0x100) % 0x100) / 255., + b = (hex % 0x100) / 255., + a = alpha, + } + ) + return err.set_type(obj, "color") +end + +local rgb = function(hex) + return rgba(hex, 1.0) +end + +-------------------------------------------------------------------------------- +-- Gradients +-- +-- these are tables like {[stop] :: color} where stop is a float between 0 and 1 +-- and color is a color as defined above + +local _make_gradient = function(colorstops, f) + local c = {} + for stop, spec in pairs(colorstops) do + assert( + stop <= 1 and stop >= 0, + "ERROR: color stop must be between 0 and 1; got " .. stop + ) + c[stop] = f(spec) + end + return err.set_type(err.safe_table(c), "gradient") +end + +-- {[stop] :: hex} -> Gradient +local gradient_rgb = function(colorstops) + return _make_gradient(colorstops, rgb) +end + +-- {[stop] :: {hex, alpha}} -> Gradient +local gradient_rgba = function(colorstops) + return _make_gradient( + colorstops, + function(spec) return rgba(spec[1], spec[2]) end + ) +end + +local compile_patterns + +compile_patterns = function(patterns) + local r = {} + for k, v in pairs(patterns) do + if type(v) == "number" then + r[k] = rgb(v) + elseif v.color ~= nil then + r[k] = rgba(v.color, v.alpha) + elseif v.gradient ~= nil then + local p = {} + local g = v.gradient + for i = 1, #g do + local _g = g[i] + p[_g.stop] = _g.color + end + r[k] = gradient_rgb(p) + elseif v.gradient_alpha ~= nil then + local p = {} + local g = v.gradient_alpha + for i = 1, #g do + local _g = g[i] + p[_g.stop] = {_g.color, _g.alpha} + end + r[k] = gradient_rgba(p) + else + r[k] = compile_patterns(v) + end + end + return r +end + +return compile_patterns diff --git a/drawing/compile.lua b/src/compile.lua similarity index 100% rename from drawing/compile.lua rename to src/compile.lua diff --git a/src/err.lua b/src/err.lua new file mode 100644 index 0000000..f97d73c --- /dev/null +++ b/src/err.lua @@ -0,0 +1,52 @@ +local M = {} + +local i_o = require 'i_o' + +M.assert_trace = function(test, msg) + if not test then + i_o.errorf(msg) + print(debug.traceback()) + os.exit(1) + end +end + +M.safe_table = function(tbl) + local ck_key = function(_, key) + local v = rawget(tbl, key) + M.assert_trace(v ~= nil, "key doesn't exist: "..key) + return v + end + return setmetatable(tbl, {__index = ck_key}) +end + +local TYPE_KEY = '__type' + +M.set_type = function(tbl, _type) + local mt = getmetatable(tbl) + mt[TYPE_KEY] = _type + return setmetatable(tbl, mt) +end + +M.get_type = function(x) + local ltype = type(x) + if ltype == "table" then + local mt = getmetatable(x) + if mt == nil then + return ltype + else + return mt[TYPE_KEY] + end + else + return ltype + end +end + +M.check_type = function(x, _type) + local xtype = nil + if x ~= nil then + xtype = M.get_type(x) + end + i_o.assertf(xtype == _type, "type must be '%s' got '%s' instead", _type, xtype) +end + +return M diff --git a/src/format.lua b/src/format.lua new file mode 100644 index 0000000..9e91967 --- /dev/null +++ b/src/format.lua @@ -0,0 +1,43 @@ +local M = {} + +local __tostring = tostring +local __math_floor = math.floor +local __math_ceil = math.ceil +local __string_format = string.format + +M.round = function(x, places) + local m = 10 ^ (places or 0) + if x >= 0 then + return __math_floor(x * m + 0.5) / m + else + return __math_ceil(x * m - 0.5) / m + end +end + +M.round_to_string = function(x, places) + places = places or 0 + if places >= 0 then + return __string_format('%.'..places..'f', x) + else + return __tostring(M.round(x, 0)) + end +end + +M.precision_round_to_string = function(x, sig_fig) + sig_fig = sig_fig or 4 + if x < 10 then return M.round_to_string(x, sig_fig - 1) + elseif x < 100 then return M.round_to_string(x, sig_fig - 2) + elseif x < 1000 then return M.round_to_string(x, sig_fig - 3) + else return M.round_to_string(x, sig_fig - 4) + end +end + +M.convert_data_val = function(x) + if x < 1024 then return '', x + elseif x < 1048576 then return 'Ki', x / 1024 + elseif x < 1073741824 then return 'Mi', x / 1048576 + else return 'Gi', x / 1073741824 + end +end + +return M diff --git a/src/i_o.lua b/src/i_o.lua new file mode 100644 index 0000000..b00da6f --- /dev/null +++ b/src/i_o.lua @@ -0,0 +1,107 @@ +local M = {} + +local __string_format = string.format +local __tonumber = tonumber +local __os_execute = os.execute +local __io_popen = io.popen +local __io_open = io.open +local __string_match = string.match +local __conky_parse = conky_parse + +-------------------------------------------------------------------------------- +-- logging/printing + +M.printf = function(fmt, ...) + print(__string_format(fmt, ...)) +end + +M.logf = function(level, fmt, ...) + M.printf(level..': '..fmt, ...) +end + +M.errorf = function(fmt, ...) + M.logf('ERROR', fmt, ...) +end + +M.warnf = function(fmt, ...) + M.logf('WARN', fmt, ...) +end + +M.infof = function(fmt, ...) + M.logf('INFO', fmt, ...) +end + +M.assertf = function(test, fmt, ...) + -- NOTE use exit here because the assert command will only break one loop + -- in the conky update cycle rather than quit the entire program + if not test then + M.errorf(fmt, ...) + print(debug.traceback()) + os.exit(1) + end +end + +-------------------------------------------------------------------------------- +-- reading files/command output + +--[[ +available modes per lua docs +*n: number (actually returns a number) +*a: entire file (default here) +*l: reads one line and strips \n (default for read cmd) +*L; reads one line and keeps \n +N: reads number of lines (where N is a number) +--]] +local read_entire_file = function(file, regex, mode) + if not file then return end + local str = file:read(mode or '*a') + file:close() + if not str then return end + if regex then return __string_match(str, regex) or '' else return str end +end + +M.read_file = function(path, regex, mode) + return read_entire_file(__io_open(path, 'rb'), regex, mode) +end + +M.execute_cmd = function(cmd, regex, mode) + return read_entire_file(__io_popen(cmd), regex, mode) +end + +-------------------------------------------------------------------------------- +-- boolean tests + +M.exit_code_cmd = function(cmd) + local _, _, rc = __os_execute(cmd) + return rc +end + +M.exe_exists = function(exe) + return M.exit_code_cmd('command -v '..exe..' > /dev/null') == 0 +end + +M.assert_exe_exists = function(exe) + M.assertf(M.exe_exists(exe), 'executable %s not found', exe) +end + +M.file_exists = function(path) + return M.exit_code_cmd('stat '..path..' > /dev/null 2>&1') == 0 +end + +M.assert_file_exists = function(path) + M.assertf(M.file_exists(path), '%s does not exist', path) +end + +-------------------------------------------------------------------------------- +-- conky object execution + +M.conky = function(expr, regex) + local ans = __conky_parse(expr) + if regex then return __string_match(ans, regex) or '' else return ans end +end + +M.conky_numeric = function(expr, regex) + return __tonumber(M.conky(expr, regex)) or 0 +end + +return M diff --git a/src/impure.lua b/src/impure.lua new file mode 100644 index 0000000..458326c --- /dev/null +++ b/src/impure.lua @@ -0,0 +1,28 @@ +local M = {} + +M.sequence = function(...) + local fs = {...} + for i = 1, #fs do + fs[i]() + end +end + +M.each = function(f, seq, ...) + for i = 1, #seq do + f(seq[i], ...) + end +end + +M.ieach = function(f, seq, ...) + for i = 1, #seq do + f(i, seq[i], ...) + end +end + +M.each2 = function(f, seq1, seq2, ...) + for i = 1, #seq1 do + f(seq1[i], seq2[i], ...) + end +end + +return M diff --git a/src/json.lua b/src/json.lua new file mode 100644 index 0000000..bf62f93 --- /dev/null +++ b/src/json.lua @@ -0,0 +1,194 @@ +local M = {} + +local __string_gsub = string.gsub +local __string_char = string.char +local __string_find = string.find +local __string_sub = string.sub +local __table_concat = table.concat +local __math_floor = math.floor +local __pairs = pairs +local __tonumber = tonumber + +local decode -- to ref this before definition + +local decode_scan_whitespace = function(s, start_pos) + local whitespace = " \n\r\t" + local string_len = #s + + while (__string_find(whitespace, __string_sub(s, start_pos, start_pos), 1, true) and + start_pos <= string_len) do + start_pos = start_pos + 1 + end + return start_pos +end + +local decode_scan_array = function(s, start_pos) + local array = {} + local string_len = #s + + start_pos = start_pos + 1 + + repeat + start_pos = decode_scan_whitespace(s, start_pos) + + local cur_char = __string_sub(s,start_pos,start_pos) + + if (cur_char == ']') then + return array, start_pos + 1 + end + + if (cur_char == ',') then + start_pos = decode_scan_whitespace(s, start_pos + 1) + end + + object, start_pos = decode(s, start_pos) + array[#array + 1] = object + until false +end + +local decode_scan_comment = function(s, start_pos) + local end_pos = __string_find(s, '*/', start_pos + 2) + return end_pos + 2 +end + +local decode_scan_constant = function(s, start_pos) + local consts = {["true"] = true, ["false"] = false, ["null"] = nil} + local const_names = {"true", "false", "null"} + + for _, k in __pairs(const_names) do + if __string_sub(s, start_pos, start_pos + #k - 1 ) == k then + return consts[k], start_pos + #k + end + end +end + +local decode_scan_number = function(s, start_pos) + local end_pos = start_pos + 1 + local string_len = #s + local acceptable_chars = "+-0123456789.e" + + while (__string_find(acceptable_chars, __string_sub(s, end_pos, end_pos), 1, true) + and end_pos <= string_len) do + end_pos = end_pos + 1 + end + + local number_string = __string_gsub(__string_sub(s, start_pos, end_pos - 1), '+', '') + return __tonumber(number_string), end_pos +end + +local decode_scan_object = function(s, start_pos) + local object = {} + local string_len = #s + local key, value + + start_pos = start_pos + 1 + + repeat + start_pos = decode_scan_whitespace(s, start_pos) + + local cur_char = __string_sub(s, start_pos, start_pos) + + if (cur_char == '}') then + return object, start_pos + 1 + end + + if (cur_char == ',') then + start_pos = decode_scan_whitespace(s, start_pos + 1) + end + + -- Scan the key + key, start_pos = decode(s, start_pos) + + start_pos = decode_scan_whitespace(s, start_pos) + start_pos = decode_scan_whitespace(s, start_pos + 1) + + value, start_pos = decode(s, start_pos) + + object[key] = value + until false +end + +local escape_sequences = { + ["\\t"] = "\t", + ["\\f"] = "\f", + ["\\r"] = "\r", + ["\\n"] = "\n", + ["\\b"] = "\b" +} + +setmetatable(escape_sequences, {__index = function(t, k) return __string_sub(k, 2) end})--skip "\" + +local decode_scan_string = function (s, start_pos) + local start_char = __string_sub(s, start_pos, start_pos) + + local t = {} + local i, j = start_pos, start_pos + + while __string_find(s, start_char, j + 1) ~= j + 1 do + local oldj = j + local x, y = __string_find(s, start_char, oldj + 1) + + i, j = __string_find(s, "\\.", j + 1) + + if not i or x < i then i, j = x, y - 1 end + + t[#t + 1] = __string_sub(s, oldj + 1, i - 1) + + if __string_sub(s, i, j) == "\\u" then + local a = __string_sub(s, j + 1, j + 4) + local n = __tonumber(a, 16) + local x + + j = j + 4 + + if n < 0x80 then + x = __string_char(n % 0x80) + elseif n < 0x800 then + x = __string_char(0xC0 + (__math_floor(n / 64) % 0x20), 0x80 + (n % 0x40)) + else + x = __string_char(0xE0 + (__math_floor(n / 4096) % 0x10), 0x80 + + (__math_floor(n / 64) % 0x40), 0x80 + (n % 0x40)) + end + + t[#t + 1] = x + else + t[#t + 1] = escape_sequences[__string_sub(s, i, j)] + end + end + t[#t + 1] = __string_sub(j, j + 1) + + return __table_concat(t, ""), j + 2 +end + +decode = function(s, start_pos) + start_pos = start_pos or 1 + start_pos = decode_scan_whitespace(s, start_pos) + + local cur_char = __string_sub(s, start_pos, start_pos) + + if cur_char == '{' then + return decode_scan_object(s, start_pos) + end + + if cur_char == '[' then + return decode_scan_array(s, start_pos) + end + + if __string_find("+-0123456789.e", cur_char, 1, true) then + return decode_scan_number(s, start_pos) + end + + if cur_char == [["]] or cur_char == [[']] then + return decode_scan_string(s, start_pos) + end + + if __string_sub(s, start_pos, start_pos + 1) == '/*' then + return decode(s, decode_scan_comment(s, start_pos)) + end + + return decode_scan_constant(s, start_pos) +end + +M.decode = decode + +return M diff --git a/drawing/common.lua b/src/modules/common.lua similarity index 96% rename from drawing/common.lua rename to src/modules/common.lua index 42ef880..88869c2 100644 --- a/drawing/common.lua +++ b/src/modules/common.lua @@ -19,43 +19,10 @@ local style = require 'style' local source = require 'source' local pure = require 'pure' -local compile_patterns - --- TODO move to color module -compile_patterns = function(patterns) - local r = {} - for k, v in pairs(patterns) do - if type(v) == "number" then - r[k] = color.rgb(v) - elseif v.color ~= nil then - r[k] = color.rgba(v.color, v.alpha) - elseif v.gradient ~= nil then - local p = {} - local g = v.gradient - for i = 1, #g do - local _g = g[i] - p[_g.stop] = _g.color - end - r[k] = color.gradient_rgb(p) - elseif v.gradient_alpha ~= nil then - local p = {} - local g = v.gradient_alpha - for i = 1, #g do - local _g = g[i] - p[_g.stop] = {_g.color, _g.alpha} - end - r[k] = color.gradient_rgba(p) - else - r[k] = compile_patterns(v) - end - end - return r -end - return function(config) local M = {} - local patterns = compile_patterns(config.theme.patterns) + local patterns = color(config.theme.patterns) local font = config.theme.font local font_sizes = font.sizes local font_family = font.family diff --git a/drawing/filesystem.lua b/src/modules/filesystem.lua similarity index 96% rename from drawing/filesystem.lua rename to src/modules/filesystem.lua index 0c783c5..4d18d56 100644 --- a/drawing/filesystem.lua +++ b/src/modules/filesystem.lua @@ -10,7 +10,7 @@ return function(config, main_state, common, width, point) ----------------------------------------------------------------------------- -- smartd - i_o.exe_assert('pidof') + i_o.assert_exe_exists('pidof') local mk_smart = function(y) local obj = common.make_text_row(point.x, y, width, 'SMART Daemon') @@ -35,6 +35,7 @@ return function(config, main_state, common, width, point) local mk_bars = function(y) local paths = pure.map_keys('path', config.fs_paths) local names = pure.map_keys('name', config.fs_paths) + impure.each(i_o.assert_file_exists, paths) local CONKY_CMDS = pure.map( pure.partial(string.format, '${fs_used_perc %s}', true), paths diff --git a/drawing/graphics.lua b/src/modules/graphics.lua similarity index 97% rename from drawing/graphics.lua rename to src/modules/graphics.lua index e63eb3c..1637311 100644 --- a/drawing/graphics.lua +++ b/src/modules/graphics.lua @@ -14,7 +14,8 @@ return function(update_freq, config, common, width, point) ----------------------------------------------------------------------------- -- nvidia state - i_o.exe_assert(NVIDIA_EXE) + i_o.assert_exe_exists(NVIDIA_EXE) + i_o.assert_file_exists(config.dev_power) -- vars to process the nv settings glob -- @@ -38,8 +39,6 @@ return function(update_freq, config, common, width, point) '(%d+),(%d+)\n'.. 'graphics=(%d+), memory=%d+, video=(%d+), PCIe=%d+\n' - local GPU_BUS_CTRL = '/sys/bus/pci/devices/0000:01:00.0/power/control' - local mod_state = { error = false, used_memory = 0, @@ -52,7 +51,7 @@ return function(update_freq, config, common, width, point) } local update_state = function() - if i_o.read_file(GPU_BUS_CTRL, nil, '*l') == 'on' then + if i_o.read_file(config.dev_power, nil, '*l') == 'on' then local nvidia_settings_glob = i_o.execute_cmd(NV_QUERY) if nvidia_settings_glob == '' then mod_state.error = 'Error' diff --git a/drawing/memory.lua b/src/modules/memory.lua similarity index 100% rename from drawing/memory.lua rename to src/modules/memory.lua diff --git a/drawing/network.lua b/src/modules/network.lua similarity index 100% rename from drawing/network.lua rename to src/modules/network.lua diff --git a/drawing/pacman.lua b/src/modules/pacman.lua similarity index 100% rename from drawing/pacman.lua rename to src/modules/pacman.lua diff --git a/drawing/power.lua b/src/modules/power.lua similarity index 100% rename from drawing/power.lua rename to src/modules/power.lua diff --git a/drawing/processor.lua b/src/modules/processor.lua similarity index 97% rename from drawing/processor.lua rename to src/modules/processor.lua index 428fb99..0801f8d 100644 --- a/drawing/processor.lua +++ b/src/modules/processor.lua @@ -39,12 +39,10 @@ return function(update_freq, config, main_state, common, width, point) if math.fmod(ncores, config.core_rows) == 0 then show_cores = true else - print( - string.format( - 'WARNING: could not evenly distribute %i cores over %i rows', - ncores, - config.core_rows - ) + i_o.warnf( + 'could not evenly distribute %i cores over %i rows; disabling', + ncores, + config.core_rows ) end end diff --git a/drawing/readwrite.lua b/src/modules/readwrite.lua similarity index 94% rename from drawing/readwrite.lua rename to src/modules/readwrite.lua index f2f1777..473f882 100644 --- a/drawing/readwrite.lua +++ b/src/modules/readwrite.lua @@ -1,6 +1,8 @@ local format = require 'format' local pure = require 'pure' local sys = require 'sys' +local i_o = require 'i_o' +local impure = require 'impure' return function(update_freq, config, common, width, point) local PLOT_SEC_BREAK = 20 @@ -9,6 +11,8 @@ return function(update_freq, config, common, width, point) local mod_state = {read = 0, write = 0} local device_paths = sys.get_disk_paths(config.devices) + impure.each(i_o.assert_file_exists, device_paths) + local update_state = function() mod_state.read, mod_state.write = sys.get_total_disk_io(device_paths) end diff --git a/drawing/system.lua b/src/modules/system.lua similarity index 100% rename from drawing/system.lua rename to src/modules/system.lua diff --git a/src/pure.lua b/src/pure.lua new file mode 100644 index 0000000..db4b93c --- /dev/null +++ b/src/pure.lua @@ -0,0 +1,259 @@ +local M = {} + +local err = require 'err' + +local __math_floor = math.floor + +-------------------------------------------------------------------------------- +-- zippy functions + +-- TODO generalize to arbitrary number of sequences +M.zip_with = function(f, seq1, seq2) + local r = {} + for i = 1, #seq1 do + r[i] = f(seq1[i], seq2[i]) + end + return r +end + +M.zip = function(...) + local seqs = {...} + local imax = math.min(table.unpack(M.map(function(t) return #t end, seqs))) + local jmax = #seqs + local r = {} + for i = 1, imax do + r[i] = {} + for j = 1, jmax do + r[i][j] = seqs[j][i] + end + end + return r +end + +M.unzip = function(seqs) + return M.zip(table.unpack(seqs)) +end + +-------------------------------------------------------------------------------- +-- reductions + +M.reduce = function(f, init, seq) + if seq == nil then + return init + else + local r = init + for i = 1, #seq do + r = f(r, seq[i]) + end + return r + end +end + +-------------------------------------------------------------------------------- +-- mappy functions + +M.map = function(f, seq, ...) + local r = {} + for i = 1, #seq do + r[i] = f(seq[i], ...) + end + return r +end + +M.map_n = function(f, n, ...) + local r = {} + for i = 1, n do + r[i] = f(i, ...) + end + return r +end + +M.imap = function(f, seq) + local r = {} + for i = 1, #seq do + r[i] = f(i, seq[i]) + end + return r +end + +M.map_keys = function(key, tbls) + local r = {} + for i = 1, #tbls do + r[i] = tbls[i][key] + end + return r +end + +M.map_at = function(key, f, tbl) + local r = {} + for k, v in pairs(tbl) do + if k == key then + r[k] = f(v) + else + r[k] = v + end + end + return r +end + +-------------------------------------------------------------------------------- +-- generations + +M.seq = function(n, start) + start = start or 1 + local r = {} + for i = 1, n do + r[i] = i + start - 1 + end + return r +end + +M.rep = function(n, x) + local r = {} + for i = 1, n do + r[i] = x + end + return r +end + +-------------------------------------------------------------------------------- +-- random list things + +M.set = function(tbl, key, value) + local r = {} + for k, v in pairs(tbl) do + if k == key then + r[k] = value + else + r[k] = v + end + end + return r +end + +M.reverse = function(xs) + local j = 1 + local r = {} + for i = #xs, 1, -1 do + r[j] = xs[i] + j = j + 1 + end + return r +end + +M.filter = function(f, seq) + local r = {} + local j = 1 + for i = 1, #seq do + if f(seq[i]) == true then + r[j] = seq[i] + j = j + 1 + end + end + return r +end + +M.flatten = function(xs) + local r = {} + for i = 1, #xs do + for j = 1, #xs[i] do + table.insert(r, xs[i][j]) + end + end + return r +end + +M.concat = function(...) + return M.flatten({...}) +end + +M.table_array = function(tbl) + local r = {} + for i = 1, #tbl do + r[i] = tbl[i] + end + return r +end + +-------------------------------------------------------------------------------- +-- functional functions + +local get_arity = function(f, args) + local i = #args + while args[i] == true do + i = i - 1 + end + if i < #args then + return table.move(args, 1, #args - i, 1, {}), #args + else + local arity = debug.getinfo(f, "u")["nparams"] + err.assert_trace(arity > #args, 'too many arguments for partial') + return args, arity + end +end + +-- poor man's Lisp macro :) +M.partial = function(f, ...) + local args, arity = get_arity(f, {...}) + local format_args = function(fmt, n, start) + return table.concat( + M.map(function(i) return string.format(fmt, i) end, M.seq(n, start)), + ',' + ) + end + local partial_args = format_args('args[%i]', #args, 1) + local rem_args = format_args('x%i', arity - #args, #args + 1) + local src = string.format( + 'return function(%s) return f(%s,%s) end', + rem_args, + partial_args, + rem_args + ) + return load(src, 'partial_apply', 't', {f = f, args = args})() +end + +M.compose = function(f, ...) + if #{...} == 0 then + return f + else + local g = M.compose(...) + return function(x) return f(g(x)) end + end +end + +-- TODO is there a way to do this without nesting a zillion function calls? +M.sequence = function(...) + local fs = {...} + return function(x) + for i = 1, #fs do + fs[i](x) + end + end +end + +M.memoize = function(f) + local mem = {} -- memoizing table + setmetatable(mem, {__mode = "kv"}) -- make it weak + return function (x, ...) + local r = mem[x] + if not r then + r = f(x, ...) + mem[x] = r + end + return r + end +end + +M.maybe = function(def, f, x) + if x == nil then + return def + else + return f(x) + end +end + +-- round to whole numbers since I don't need more granularity and extra values +-- will lead to cache misses +M.round_percent = __math_floor + +return M diff --git a/src/sys.lua b/src/sys.lua new file mode 100644 index 0000000..0f0cbce --- /dev/null +++ b/src/sys.lua @@ -0,0 +1,332 @@ +local M = {} + +local i_o = require 'i_o' +local pure = require 'pure' + +local __string_match = string.match +local __string_gmatch = string.gmatch +local __string_format = string.format +local __tonumber = tonumber + +local dirname = function(s) + return __string_match(s, '(.*)/name') +end + +local read_micro = function(path) + return i_o.read_file(path, nil, '*n') * 0.000001 +end + +-------------------------------------------------------------------------------- +-- memory + +local MEMINFO_PATH = '/proc/meminfo' + +local fmt_mem_field = function(field) + return field..':%s+(%d+)' +end + +local meminfo_regex = function(read_swap) + -- ASSUME the order of the meminfo file will never change, but some options + -- (like swap) might not exist + local free_fields = { + 'MemFree', + 'Buffers', + 'Cached' + } + local swap_field = 'SwapFree' + local slab_fields = { + 'Shmem', + 'SReclaimable' + } + local all_fields = read_swap == true + and {free_fields, {swap_field}, slab_fields} + or {free_fields, slab_fields} + local patterns = pure.map(fmt_mem_field, pure.flatten(all_fields)) + return table.concat(patterns, '.+\n') +end + +M.meminfo_updater_swap = function(mem_state, swap_state) + local regex = meminfo_regex(true) + return function() + mem_state.memfree, + mem_state.buffers, + mem_state.cached, + swap_state.free, + mem_state.shmem, + mem_state.sreclaimable + = __string_match(i_o.read_file(MEMINFO_PATH), regex) + end +end + +M.meminfo_updater_noswap = function(mem_state) + local regex = meminfo_regex(false) + return function() + mem_state.memfree, + mem_state.buffers, + mem_state.cached, + mem_state.shmem, + mem_state.sreclaimable + = __string_match(i_o.read_file(MEMINFO_PATH), regex) + end +end + +M.meminfo_field_reader = function(field) + local pattern = fmt_mem_field(field) + return function() + return tonumber(i_o.read_file(MEMINFO_PATH, pattern)) + end +end + +-------------------------------------------------------------------------------- +-- intel powercap + +local SYSFS_RAPL = '/sys/class/powercap' + +M.intel_powercap_reader = function(dev) + local uj = __string_format('%s/%s/energy_uj', SYSFS_RAPL, dev) + i_o.assert_file_exists(uj) + return function() + return read_micro(uj) + end +end + +-------------------------------------------------------------------------------- +-- battery + +local SYSFS_POWER = '/sys/class/power_supply' + +local format_power_path = function(battery, property) + local p = __string_format('%s/%s/%s', SYSFS_POWER, battery, property) + i_o.assert_file_exists(p) + return p +end + +M.battery_power_reader = function(battery) + local current = format_power_path(battery, 'current_now') + local voltage = format_power_path(battery, 'voltage_now') + return function() + return read_micro(current) * read_micro(voltage) + end +end + +M.battery_status_reader = function(battery) + local status = format_power_path(battery, 'status') + return function() + return i_o.read_file(status, nil, '*l') ~= 'Discharging' + end +end + +-------------------------------------------------------------------------------- +-- disk io + +M.get_disk_paths = function(devs) + return pure.map(pure.partial(string.format, '/sys/block/%s/stat', true), devs) +end + +-- fields 3 and 7 (sectors read and written) +local RW_REGEX = '%s+%d+%s+%d+%s+(%d+)%s+%d+%s+%d+%s+%d+%s+(%d+)' + +-- the sector size of any block device in linux is 512 bytes +-- see https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/types.h?id=v4.4-rc6#n121 +local BLOCK_SIZE_BYTES = 512 + +M.get_disk_io = function(path) + local r, w = __string_match(i_o.read_file(path), RW_REGEX) + return __tonumber(r) * BLOCK_SIZE_BYTES, __tonumber(w) * BLOCK_SIZE_BYTES +end + +M.get_total_disk_io = function(paths) + local r = 0 + local w = 0 + for i = 1, #paths do + local _r, _w = M.get_disk_io(paths[i]) + r = r + _r + w = w + _w + end + return r, w +end + +-------------------------------------------------------------------------------- +-- network + +-- ASSUME realpath exists (part of coreutils) + +local get_interfaces = function() + local s = i_o.execute_cmd('realpath /sys/class/net/* | grep -v virtual') + local interfaces = {} + for iface in __string_gmatch(s, '/([^/\n]+)\n') do + interfaces[#interfaces + 1] = iface + end + return interfaces +end + +M.get_net_interface_paths = function() + local is = get_interfaces() + return pure.map( + function(s) + local dir = string.format('/sys/class/net/%s/statistics/', s) + return {rx = dir..'rx_bytes', tx = dir..'tx_bytes'} + end, + is + ) +end + +-------------------------------------------------------------------------------- +-- cpu + +-- ASSUME nproc and lscpu will always be available + +M.get_core_number = function() + return tonumber(i_o.read_file('/proc/cpuinfo', 'cpu cores%s+:%s(%d+)')) +end + +M.get_cpu_number = function() + return tonumber(i_o.execute_cmd('nproc', nil, '*n')) +end + +-- TODO what if this fails? +local get_coretemp_dir = function() + i_o.assert_exe_exists('grep') + local s = i_o.execute_cmd('grep -l \'^coretemp$\' /sys/class/hwmon/*/name') + return dirname(s) +end + +-- map cores to integer values starting at 1; this is necessary since some cpus +-- don't report their core id's as a sequence of integers starting at 0 +local get_core_id_mapper = function() + local s = i_o.execute_cmd('lscpu -p=CORE | tail -n+5 | sort | uniq') + local m = {} + local i = 1 + for core_id in string.gmatch(s, '(%d+)') do + m[tonumber(core_id)] = i + i = i + 1 + end + return m +end + +local get_core_mappings = function() + local ncpus = M.get_cpu_number() + local ncores = M.get_core_number() + local nthreads = ncpus / ncores + local core_id_mapper = get_core_id_mapper() + local conky_thread_ids = pure.rep(ncores, nthreads) + local core_mappings = {} + local s = i_o.execute_cmd('lscpu -p=cpu,CORE | tail -n+5') + for cpu_id, core_id in string.gmatch(s, '(%d+),(%d+)') do + local conky_core_id = core_id_mapper[tonumber(core_id)] + local conky_cpu_id = tonumber(cpu_id) + 1 + core_mappings[conky_cpu_id] = { + conky_core_id = conky_core_id, + conky_thread_id = conky_thread_ids[conky_core_id], + } + conky_thread_ids[conky_core_id] = conky_thread_ids[conky_core_id] - 1 + end + return core_mappings +end + +M.get_coretemp_paths = function() + local d = get_coretemp_dir() + i_o.assert_exe_exists('grep') + local s = i_o.execute_cmd(string.format('grep Core %s/temp*_label', d)) + local ps = {} + local core_id_mapper = get_core_id_mapper() + for temp_name, core_id in string.gmatch(s, '/([^/\n]+)_label:Core (%d+)\n') do + ps[core_id_mapper[tonumber(core_id)]] = string.format('%s/%s_input', d, temp_name) + end + return ps +end + +M.read_freq = function() + -- NOTE: Using the builtin conky functions for getting cpu freq seems to make + -- the entire loop jittery due to high variance latency. Querying + -- scaling_cur_freq in sysfs seems to do the same thing. It appears lscpu + -- (which queries /proc/cpuinfo) is much faster and doesn't have this jittery + -- problem. + local c = i_o.execute_cmd('lscpu -p=MHZ') + local f = 0 + local n = 0 + for s in __string_gmatch(c, '(%d+%.%d+)') do + f = f + __tonumber(s) + n = n + 1 + end + return __string_format('%.0f Mhz', f / n) +end + +M.get_hwp_paths = function() + return pure.map_n( + function(i) + return '/sys/devices/system/cpu/cpu' + .. (i - 1) + .. '/cpufreq/energy_performance_preference' + end, + M.get_cpu_number() + ) +end + +M.read_hwp = function(hwp_paths) + -- read HWP of first cpu, then test all others to see if they match + local hwp_pref = i_o.read_file(hwp_paths[1], nil, "*l") + local mixed = false + local i = 2 + + while not mixed and i <= #hwp_paths do + if hwp_pref ~= i_o.read_file(hwp_paths[i], nil, '*l') then + mixed = true + end + i = i + 1 + end + + if mixed then + return 'Mixed' + elseif hwp_pref == 'power' then + return 'Power' + elseif hwp_pref == 'balance_power' then + return 'Bal. Power' + elseif hwp_pref == 'balance_performance' then + return 'Bal. Performance' + elseif hwp_pref == 'performance' then + return 'Performance' + elseif hwp_pref == 'default' then + return 'Default' + else + return 'Unknown' + end +end + +M.init_cpu_loads = function() + local m = get_core_mappings() + local cpu_loads = {} + for cpu_id, core in pairs(m) do + cpu_loads[cpu_id] = { + active_prev = 0, + total_prev = 0, + percent_active = 0, + conky_core_id = core.conky_core_id, + conky_thread_id = core.conky_thread_id, + } + end + return cpu_loads +end + +M.read_cpu_loads = function(cpu_loads) + local ncpus = #cpu_loads + local i = 1 + local iter = io.lines('/proc/stat') + iter() -- ignore first line + for ln in iter do + if i > ncpus then break end + local user, system, idle = __string_match(ln, '(%d+) %d+ (%d+) (%d+)', 5) + local active = user + system + local total = active + idle + local c = cpu_loads[i] + if total > c.total_prev then -- guard against 1/0 errors + c.percent_active = (active - c.active_prev) / (total - c.total_prev) + c.active_prev = active + c.total_prev = total + end + i = i + 1 + end + return cpu_loads +end + +return M diff --git a/src/widget/arc/arc.lua b/src/widget/arc/arc.lua new file mode 100644 index 0000000..cafe78e --- /dev/null +++ b/src/widget/arc/arc.lua @@ -0,0 +1,29 @@ +local M = {} + +local geom = require 'geom' +local circle = require 'circle' +local path = require 'path' +local shape = require 'shape' + +-------------------------------------------------------------------------------- +-- pure + +M.make_shape = function(arc, thickness, pattern) + return shape.shape( + path.create_arc_from_geom(geom.CR_DUMMY, arc), + circle.make_source(pattern, arc.center, arc.radius, thickness) + ) +end + +M.make = function(arc, config) + return circle._make(arc, config, M.make_shape) +end + +-------------------------------------------------------------------------------- +-- impure + +M.config = circle.config + +M.draw = circle.draw + +return M diff --git a/src/widget/arc/circle.lua b/src/widget/arc/circle.lua new file mode 100644 index 0000000..cee8273 --- /dev/null +++ b/src/widget/arc/circle.lua @@ -0,0 +1,55 @@ +local M = {} + +local geom = require 'geom' +local err = require 'err' +local source = require 'source' +local style = require 'style' +local path = require 'path' +local shape = require 'shape' + +-------------------------------------------------------------------------------- +-- pure + +M.make_pattern_radii = function(r, t) + return r - t * 0.5, r + t * 0.5 +end + +M.make_source = function(pattern, center, radius, thickness) + local r1, r2 = M.make_pattern_radii(radius, thickness) + return source.radial_pattern(pattern, center, r1, r2) +end + +M.make_shape = function(circle, thickness, pattern) + return shape.shape( + path.create_circle_from_geom(geom.CR_DUMMY, circle), + M.make_source(pattern, circle.center, circle.radius, thickness) + ) +end + +M._make = function(_geom, config, make_shape_fun) + return shape.styled_shape( + config.style, + make_shape_fun, + _geom, + config.style.thickness, + config.pattern + ) +end + +M.make = function(circle, config) + return M._make(circle, config, M.make_shape) +end + +M.config = function(_style, pattern) + return err.safe_table({style = _style, pattern = pattern}) +end + +-------------------------------------------------------------------------------- +-- impure + +M.draw = function(obj, cr) + style.set_line_style(obj.style, cr) + shape.draw_shape(obj.shape, cr) +end + +return M diff --git a/src/widget/arc/compound_dial.lua b/src/widget/arc/compound_dial.lua new file mode 100644 index 0000000..d57011a --- /dev/null +++ b/src/widget/arc/compound_dial.lua @@ -0,0 +1,60 @@ +local M = {} + +local arc = require 'arc' +local dial = require 'dial' +local pure = require 'pure' +local impure = require 'impure' +local style = require 'style' +local shape = require 'shape' +local dynamic = require 'dynamic' + +-------------------------------------------------------------------------------- +-- pure + +M.make = function(_arc, bg_config, fg_threshold_config, inner_radius, num_dials) + local t = bg_config.style.thickness + + local spacing = (t * num_dials - _arc.radius + inner_radius) + / (1 - num_dials) + assert(spacing >= 0, "ERROR: compound dial spacing is negative") + local arcs = pure.map_n( + function(i) + return pure.set( + _arc, + "radius", + inner_radius + t * 0.5 + (i - 1) * (spacing + t) + ) + end, + num_dials + ) + local setters = pure.map(dial.make_setter, arcs, t, fg_threshold_config) + + return dynamic.compound( + { + shapes = pure.map(arc.make_shape, arcs, t, bg_config.pattern), + style = bg_config.style, + }, + setters, + 0 + ) +end + +-------------------------------------------------------------------------------- +-- impure + +M.set = function(obj, i, percent) + obj.var[i] = obj.setters[i](percent) +end + +M.draw_static = function(obj, cr) + local static = obj.static + style.set_line_style(static.style, cr) + impure.each(shape.draw_shape, static.shapes, cr) +end + +M.draw_dynamic = function(obj, cr) + style.set_line_style(obj.static.style, cr) + impure.each(shape.draw_shape, obj.var, cr) +end + +return M diff --git a/src/widget/arc/dial.lua b/src/widget/arc/dial.lua new file mode 100644 index 0000000..7f9b920 --- /dev/null +++ b/src/widget/arc/dial.lua @@ -0,0 +1,75 @@ +local M = {} + +local geom = require 'geom' +local pure = require 'pure' +local arc = require 'arc' +local circle = require 'circle' +local source = require 'source' +local style = require 'style' +local path = require 'path' +local shape = require 'shape' +local dynamic = require 'dynamic' + +-------------------------------------------------------------------------------- +-- pure + +M.make_setter = function(_arc, thickness, threshold_config) + local c = _arc.center + local r = _arc.radius + local t1 = _arc.theta0 + local t2 = _arc.theta1 + local r1, r2 = circle.make_pattern_radii(r, thickness) + local source_chooser = source.radial_pattern_chooser( + threshold_config.low_pattern, + threshold_config.high_pattern, + threshold_config.threshold, + c, + r1, + r2 + ) + local f = pure.memoize( + function(percent) + return shape.shape( + path.create_arc( + geom.CR_DUMMY, + c.x, + c.y, + r, + t1, + t1 + (percent / 100) * (t2 - t1) + ), + source_chooser(percent) + ) + end + ) + return function(percent) + return f(pure.round_percent(percent)) + end +end + +M.make = function(_arc, bg_config, fg_threshold_config) + local setter = M.make_setter( + _arc, + bg_config.style.thickness, + fg_threshold_config + ) + return dynamic.single(arc.make(_arc, bg_config), setter, 0) +end + +-------------------------------------------------------------------------------- +-- impure + +M.set = function(obj, percent) + obj.var = obj.setter(percent) +end + +M.draw_static = function(obj, cr) + arc.draw(obj.static, cr) +end + +M.draw_dynamic = function(obj, cr) + style.set_line_style(obj.static.style, cr) + shape.draw_shape(obj.var, cr) +end + +return M diff --git a/src/widget/dynamic.lua b/src/widget/dynamic.lua new file mode 100644 index 0000000..6b442c3 --- /dev/null +++ b/src/widget/dynamic.lua @@ -0,0 +1,45 @@ +local M = {} + +local err = require 'err' +local pure = require 'pure' + +-- TODO generalize these +-- types of dynamic mappers +-- 1 -> 1 +-- 1 -> 1 with recursion +-- 1 -> many +-- 1 -> many (nested in many) +-- many -> many + +M.single = function(static, setter, init) + return err.safe_table( + { + static = static, + setter = setter, + var = setter(init) + } + ) +end + +-- TODO think of a better more mathy name +M.multi = function(static, setter, inits) + return err.safe_table( + { + static = static, + setter = setter, + var = pure.map(setter, inits) + } + ) +end + +M.compound = function(static, setters, init) + return err.safe_table( + { + static = static, + setters = setters, + var = pure.map(function(setter) return setter(init) end, setters), + } + ) +end + +return M diff --git a/src/widget/geom.lua b/src/widget/geom.lua new file mode 100644 index 0000000..3d907d8 --- /dev/null +++ b/src/widget/geom.lua @@ -0,0 +1,60 @@ +local M = {} + +local err = require 'err' + +-- TODO this is weird to have here +-- dummy drawing surface +local cs = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1366, 768) +M.CR_DUMMY = cairo_create(cs) +cairo_surface_destroy(cs) +cs = nil + +M.make_point = function(x, y) + return err.safe_table({x = x, y = y}) +end + +M.make_box_at_point = function(p, w, h) + return err.safe_table( + { + corner = p, + width = w, + height = h, + -- TODO these might be unnecessary + right_x = w + p.x, + bottom_y = h + p.y + } + ) +end + +M.make_box = function(x, y, w, h) + return M.make_box_at_point(M.make_point(x, y), w, h) +end + +M.make_arc_at_point = function(p, r, t1, t2) + return err.safe_table( + { + center = p, + radius = r, + theta0 = t1, + theta1 = t2, + } + ) +end + +M.make_arc = function(x, y, r, t0, t1) + return M.make_arc_at_point(M.make_point(x, y), r, t0, t1) +end + +M.make_circle_at_point = function(p, r) + return err.safe_table({center = p, radius = r}) +end + +M.make_circle = function(x, y, r) + return M.make_circle_at_point(M.make_point(x, y), r) +end + +M.make_line = function(p1, p2) + return err.safe_table({p1 = p1, p2 = p2}) +end + +return M diff --git a/src/widget/image/Image.lua b/src/widget/image/Image.lua new file mode 100644 index 0000000..8db4140 --- /dev/null +++ b/src/widget/image/Image.lua @@ -0,0 +1,31 @@ +local M = {} + +local __imlib_load_image = imlib_load_image +local __imlib_context_set_image = imlib_context_set_image +local __imlib_render_image_on_drawable = imlib_render_image_on_drawable +local __imlib_free_image = imlib_free_image +local __imlib_image_get_width = imlib_image_get_width +local __imlib_image_get_height = imlib_image_get_height + +local set = function(obj, path) + local img = __imlib_load_image(path) + __imlib_context_set_image(img) + + obj.width = __imlib_image_get_width() + obj.height = __imlib_image_get_height() + obj.path = path + + __imlib_free_image() +end + +local draw = function(obj) + local img = __imlib_load_image(obj.path) + __imlib_context_set_image(img) + __imlib_render_image_on_drawable(obj.x, obj.y) + __imlib_free_image() +end + +M.set = set +M.draw = draw + +return M diff --git a/src/widget/image/ScaledImage.lua b/src/widget/image/ScaledImage.lua new file mode 100644 index 0000000..fc6b26a --- /dev/null +++ b/src/widget/image/ScaledImage.lua @@ -0,0 +1,31 @@ +local M = {} + +local __imlib_load_image = imlib_load_image +local __imlib_context_set_image = imlib_context_set_image +local __imlib_render_image_on_drawable_at_size = imlib_render_image_on_drawable_at_size +local __imlib_free_image = imlib_free_image +local __imlib_image_get_width = imlib_image_get_width +local __imlib_image_get_height = imlib_image_get_height + +local set = function(obj, path) + local img = __imlib_load_image(path) + __imlib_context_set_image(img) + + obj.img_width = __imlib_image_get_width() + obj.img_height = __imlib_image_get_height() + obj.path = path + + __imlib_free_image() +end + +local draw = function(obj) + local img = __imlib_load_image(obj.path) + __imlib_context_set_image(img) + __imlib_render_image_on_drawable_at_size(obj.x, obj.y, obj.width, obj.height) + __imlib_free_image() +end + +M.set = set +M.draw = draw + +return M diff --git a/src/widget/line/bar.lua b/src/widget/line/bar.lua new file mode 100644 index 0000000..b2bdc84 --- /dev/null +++ b/src/widget/line/bar.lua @@ -0,0 +1,61 @@ +local M = {} + +local geom = require 'geom' +local line = require 'line' +local source = require 'source' +local pure = require 'pure' +local path = require 'path' +local shape = require 'shape' +local dynamic = require 'dynamic' + +-------------------------------------------------------------------------------- +-- pure + +M.make_setter = function(_line, config, threshold_config) + local p1 = _line.p1 + local p2 = _line.p2 + local _p1, _p2 = line.get_wide_pattern_points(_line, config.style.thickness, config.is_wide_pattern) + + local source_chooser = source.linear_pattern_chooser( + threshold_config.low_pattern, + threshold_config.high_pattern, + threshold_config.threshold, + _p1, + _p2 + ) + local f = pure.memoize( + function(percent) + local frac = percent / 100 + local mp = geom.make_point( + (p2.x - p1.x) * frac + p1.x, + (p2.y - p1.y) * frac + p1.y + ) + return shape.shape( + path.create_line(geom.CR_DUMMY, p1, mp), + source_chooser(percent) + ) + end + ) + return function(percent) + return f(pure.round_percent(percent)) + end +end + +M.make = function(p1, p2, bg_config, fg_threshold_config) + local setter = M.make_setter(geom.make_line(p1, p2), bg_config, fg_threshold_config) + return dynamic.single(line.make(p1, p2, bg_config), setter, 0) +end + +-------------------------------------------------------------------------------- +-- impure + +M.set = function(obj, percent) + obj.var = obj.setter(percent) +end + +M.draw = function(obj, cr) + line.draw(obj.static, cr) + shape.draw_shape(obj.var, cr) +end + +return M diff --git a/src/widget/line/compound_bar.lua b/src/widget/line/compound_bar.lua new file mode 100644 index 0000000..d55286a --- /dev/null +++ b/src/widget/line/compound_bar.lua @@ -0,0 +1,67 @@ +local M = {} + +local geom = require 'geom' +local bar = require 'bar' +local dynamic = require 'dynamic' +local line = require 'line' +local style = require 'style' +local pure = require 'pure' +local impure = require 'impure' +local shape = require 'shape' + +-------------------------------------------------------------------------------- +-- pure + +-- TODO make this handle vertical bars +M.make = function(point, length, bg_config, fg_threshold_config, spacing, + num_bars, is_vertical) + local lines = pure.map_n( + function(i) + local y = (i - 1) * spacing + point.y + local p1 = geom.make_point(point.x, y) + local p2 = geom.make_point(point.x + length, y) + return geom.make_line(p1, p2) + end, + num_bars + ) + + local setters = pure.map( + bar.make_setter, + lines, + bg_config, + fg_threshold_config + ) + + local bs = bg_config.style + local static = { + shapes = pure.map( + line.make_shape, + lines, + bg_config.pattern, + bs.thickness, + bg_config.is_wide_pattern + ), + style = bs, + } + return dynamic.compound(static, setters, 0) +end + +-------------------------------------------------------------------------------- +-- pure + +M.set = function(obj, i, percent) + obj.var[i] = obj.setters[i](percent) +end + +M.draw_static = function(obj, cr) + local static = obj.static + style.set_line_style(static.style, cr) + impure.each(shape.draw_shape, static.shapes, cr) +end + +M.draw_dynamic = function(obj, cr) + style.set_line_style(obj.static.style, cr) + impure.each(shape.draw_shape, obj.var, cr) +end + +return M diff --git a/src/widget/line/line.lua b/src/widget/line/line.lua new file mode 100644 index 0000000..7c17d3f --- /dev/null +++ b/src/widget/line/line.lua @@ -0,0 +1,74 @@ +local M = {} + +local geom = require 'geom' +local err = require 'err' +local source = require 'source' +local style = require 'style' +local path = require 'path' +local shape = require 'shape' + +local __math_atan2 = math.atan2 +local __math_sin = math.sin +local __math_cos = math.cos + +-------------------------------------------------------------------------------- +-- pure + +M.config = function(_style, pattern, is_wide_pattern) + return err.safe_table( + { + style = _style, + pattern = pattern, + is_wide_pattern = is_wide_pattern or false, + } + ) +end + +M.get_wide_pattern_points = function(line, thickness, is_wide) + local p1 = line.p1 + local p2 = line.p2 + if is_wide then + local theta = __math_atan2(p2.y - p1.y, p2.x - p1.x) + local delta_x = 0.5 * thickness * __math_sin(theta) --and yes, these are actually flipped + local delta_y = 0.5 * thickness * __math_cos(theta) + local _p1 = geom.make_point(p1.x + delta_x, p1.y + delta_y) + local _p2 = geom.make_point(p1.x - delta_x, p1.y - delta_y) + return _p1, _p2 + else + return p1, p2 + end +end + +M.make_source = function(line, thickness, pattern, is_wide) + local p1, p2 = M.get_wide_pattern_points(line, thickness, is_wide) + return source.linear_pattern(pattern, p1, p2) +end + +M.make_shape = function(line, pattern, thickness, is_wide) + return shape.shape( + path.create_line_from_geom(geom.CR_DUMMY, line), + M.make_source(line, thickness, pattern, is_wide) + ) +end + +M.make = function(line, config) + local _style = config.style + return shape.styled_shape( + _style, + M.make_shape, + line, + config.pattern, + _style.thickness, + config.is_wide_pattern + ) +end + +-------------------------------------------------------------------------------- +-- impure + +M.draw = function(obj, cr) + style.set_line_style(obj.style, cr) + shape.draw_shape(obj.shape, cr) +end + +return M diff --git a/src/widget/path.lua b/src/widget/path.lua new file mode 100644 index 0000000..bfc794f --- /dev/null +++ b/src/widget/path.lua @@ -0,0 +1,73 @@ +local M = {} + +local impure = require 'impure' + +local __cairo_new_path = cairo_new_path +local __cairo_line_to = cairo_line_to +local __cairo_copy_path = cairo_copy_path +local __cairo_close_path = cairo_close_path +local __cairo_arc = cairo_arc +local __cairo_rectangle = cairo_rectangle +local __math_rad = math.rad + +local line_to = function(p, cr) + __cairo_line_to(cr, p.x, p.y) +end + +local create_poly = function(cr, points) + __cairo_new_path(cr) + impure.each(line_to, points, cr) +end + +local copy_path = function(cr) + local path = __cairo_copy_path(cr) + __cairo_new_path(cr) -- clear path to keep it from reappearing + return path +end + +M.create_open_poly = function(cr, points) + create_poly(cr, points) + return copy_path(cr) +end + +M.create_closed_poly = function(cr, points) + create_poly(cr, points) + __cairo_close_path(cr) + return copy_path(cr) +end + +M.create_line = function(cr, p1, p2) + return M.create_open_poly(cr, {p1, p2}) +end + +M.create_line_from_geom = function(cr, line) + return M.create_line(cr, line.p1, line.p2) +end + +M.create_arc = function(cr, x, y, radius, theta1, theta2) + __cairo_new_path(cr) + __cairo_arc(cr, x, y, radius, __math_rad(theta1), __math_rad(theta2)) + return copy_path(cr) +end + +M.create_arc_from_geom = function(cr, arc) + local c = arc.center + return M.create_arc(cr, c.x, c.y, arc.radius, arc.theta0, arc.theta1) +end + +M.create_circle = function(cr, x, y, radius) + return M.create_arc(cr, x, y, radius, 0, 360) +end + +M.create_circle_from_geom = function(cr, circle) + local c = circle.center + return M.create_circle(cr, c.x, c.y, circle.radius) +end + +M.create_rect = function(cr, x, y, w, h) + __cairo_new_path(cr) + __cairo_rectangle(cr, x, y, w, h) + return copy_path(cr) +end + +return M diff --git a/src/widget/rect/fill_rect.lua b/src/widget/rect/fill_rect.lua new file mode 100644 index 0000000..bc9a8fd --- /dev/null +++ b/src/widget/rect/fill_rect.lua @@ -0,0 +1,34 @@ +local M = {} + +local geom = require 'geom' +local source = require 'source' +local path = require 'path' +local style = require 'style' +local shape = require 'shape' + +-------------------------------------------------------------------------------- +-- pure + +local make_shape = function(box, line_pattern, fill_pattern) + local p1 = box.corner + local p2 = geom.make_point(p1.x, p1.y + box.height) + return shape.filled_shape( + path.create_rect(geom.CR_DUMMY, p1.x, p1.y, box.width, box.height), + source.linear_pattern(line_pattern, p1, p2), + source.linear_pattern(fill_pattern, p1, p2) + ) +end + +M.make = function(box, config, fill_pattern) + return shape.styled_shape(config.style, make_shape, box, config.pattern, fill_pattern) +end + +-------------------------------------------------------------------------------- +-- impure + +M.draw = function(obj, cr) + style.set_closed_poly_style(obj.style, cr) + shape.draw_filled_shape(obj.shape, cr) +end + +return M diff --git a/src/widget/rect/rect.lua b/src/widget/rect/rect.lua new file mode 100644 index 0000000..5f5cb0a --- /dev/null +++ b/src/widget/rect/rect.lua @@ -0,0 +1,40 @@ +local M = {} + +local geom = require 'geom' +local err = require 'err' +local source = require 'source' +local style = require 'style' +local path = require 'path' +local shape = require 'shape' + +M.config = function(closed_poly_style, pattern) + return err.safe_table( + { + style = closed_poly_style, + pattern = pattern, + } + ) +end + +local make_shape = function(box, pattern) + local p = box.corner + return shape.shape( + path.create_rect(geom.CR_DUMMY, p.x, p.y, box.width, box.height), + source.linear_pattern( + pattern, + p, + geom.make_point(p.x, p.y + box.height) + ) + ) +end + +M.make = function(box, config) + return shape.styled_shape(config.style, make_shape, box, config.pattern) +end + +M.draw = function(obj, cr) + style.set_closed_poly_style(obj.style, cr) + shape.draw_shape(obj.shape, cr) +end + +return M diff --git a/src/widget/shape.lua b/src/widget/shape.lua new file mode 100644 index 0000000..0db99fc --- /dev/null +++ b/src/widget/shape.lua @@ -0,0 +1,70 @@ +local M = {} + +local impure = require 'impure' +local err = require 'err' + +local __cairo_append_path = cairo_append_path +local __cairo_fill_preserve = cairo_fill_preserve +local __cairo_set_source = cairo_set_source +local __cairo_stroke = cairo_stroke + +-------------------------------------------------------------------------------- +-- pure + +M.shape = function(path, source) + return {path = path, source = source} +end + +M.filled_shape = function(path, line_source, fill_source) + return {path = path, line_source = line_source, fill_source = fill_source} +end + +M.shapes = function(paths, source) + return {paths = paths, source = source} +end + +M.styled_shape = function(style, make_shape_fun, geom, ...) + return err.safe_table( + { + style = style, + shape = make_shape_fun(geom, ...), + } + ) +end + +-------------------------------------------------------------------------------- +-- impure + +M.draw_path_with_source = function(path, cr, source) + __cairo_append_path(cr, path) + __cairo_set_source(cr, source) + __cairo_stroke(cr) +end + +M.draw_shape = function(shape, cr) + M.draw_path_with_source(shape.path, cr, shape.source) +end + +M.draw_filled_shape = function(shape, cr) + __cairo_append_path(cr, shape.path) + __cairo_set_source(cr, shape.fill_source) + __cairo_fill_preserve(cr) + __cairo_set_source(cr, shape.line_source) + __cairo_stroke(cr) +end + +local append_path = function(p, cr) + __cairo_append_path(cr, p) +end + +M.draw_paths_with_source = function(paths, cr, source) + impure.each(append_path, paths, cr) + __cairo_set_source(cr, source) + __cairo_stroke(cr) +end + +M.draw_shapes = function(shapes, cr) + M.draw_paths_with_source(shapes.paths, cr, shapes.source) +end + +return M diff --git a/src/widget/source.lua b/src/widget/source.lua new file mode 100644 index 0000000..8fca3d2 --- /dev/null +++ b/src/widget/source.lua @@ -0,0 +1,117 @@ +local M = {} + +local err = require 'err' + +local __cairo_pattern_create_rgba = cairo_pattern_create_rgba +local __cairo_pattern_create_radial = cairo_pattern_create_radial +local __cairo_pattern_create_linear = cairo_pattern_create_linear +local __cairo_pattern_add_color_stop_rgba = cairo_pattern_add_color_stop_rgba + +M.threshold_config = function(low_pattern, high_pattern, threshold) + return err.safe_table( + { + low_pattern = low_pattern, + high_pattern = high_pattern, + threshold = threshold, + } + ) +end + +local set_color_stops = function(pattern, colorstops) + for stop, color in pairs(colorstops) do + __cairo_pattern_add_color_stop_rgba( + pattern, + stop, + color.r, + color.g, + color.b, + color.a + ) + end +end + +local _solid_color = function(color) + return __cairo_pattern_create_rgba(color.r, color.g, color.b, color.a) +end + +local _linear_gradient = function(gradient, p1, p2) + local pattern = __cairo_pattern_create_linear(p1.x, p1.y, p2.x, p2.y) + set_color_stops(pattern, gradient) + return pattern +end + +local _radial_gradient = function(gradient, p, r1, r2) + local pattern = __cairo_pattern_create_radial(p.x, p.y, r1, p.x, p.y, r2) + set_color_stops(pattern, gradient) + return pattern +end + +local _is_gradient = function(spec) + return err.get_type(spec) == "gradient" +end + +local _create_critical_function = function(limit) + if limit then + return function(n) return (n > limit) end + else + return function(_) return nil end + end +end + +M.solid_color = function(spec) + err.check_type(spec, "color") + return _solid_color(spec) +end + +M.linear_pattern = function(spec, p1, p2) + if _is_gradient(spec) then + return _linear_gradient(spec, p1, p2) + else + return _solid_color(spec) + end +end + +M.radial_pattern = function(spec, p, r1, r2) + if _is_gradient(spec) then + return _radial_gradient(spec, p, r1, r2) + else + return _solid_color(spec) + end +end + +local source_chooser = function(low_source, high_source, limit) + local f = _create_critical_function(limit) + return function(value) + if f(value) then + return high_source + else + return low_source + end + end +end + +M.solid_color_chooser = function(low_color, high_color, limit) + return source_chooser( + M.solid_color(low_color), + M.solid_color(high_color), + limit + ) +end + +M.linear_pattern_chooser = function(low_pattern, high_pattern, limit, p1, p2) + return source_chooser( + M.linear_pattern(low_pattern, p1, p2), + M.linear_pattern(high_pattern, p1, p2), + limit + ) +end + +M.radial_pattern_chooser = function(low_pattern, high_pattern, limit, p, r1, r2) + return source_chooser( + M.radial_pattern(low_pattern, p, r1, r2), + M.radial_pattern(high_pattern, p, r1, r2), + limit + ) +end + +return M diff --git a/src/widget/style.lua b/src/widget/style.lua new file mode 100644 index 0000000..ced0e33 --- /dev/null +++ b/src/widget/style.lua @@ -0,0 +1,55 @@ +local M = {} + +local err = require 'err' + +local __cairo_set_line_width = cairo_set_line_width +local __cairo_set_line_cap = cairo_set_line_cap +local __cairo_set_line_join = cairo_set_line_join + +-------------------------------------------------------------------------------- +-- pure + +-- circle: a closed path with no corners +M.circle = function(thickness) + return err.safe_table({thickness = thickness}) +end + +-- line: an open path with no corners +M.line = function(thickness, cap) + return err.safe_table({thickness = thickness, cap = cap}) +end + +-- open poly: an open path with corners +M.open_poly = function(thickness, cap, join) + return err.safe_table({thickness = thickness, cap = cap, join = join}) +end + +-- closed poly: an closed path with corners +M.closed_poly = function(thickness, join) + return err.safe_table({thickness = thickness, join = join}) +end + +-------------------------------------------------------------------------------- +-- impure + +M.set_circle_style = function(style, cr) + __cairo_set_line_width(cr, style.thickness) +end + +M.set_line_style = function(style, cr) + __cairo_set_line_width(cr, style.thickness) + __cairo_set_line_cap(cr, style.cap) +end + +M.set_open_poly_style = function(style, cr) + __cairo_set_line_width(cr, style.thickness) + __cairo_set_line_cap(cr, style.cap) + __cairo_set_line_join(cr, style.join) +end + +M.set_closed_poly_style = function(style, cr) + __cairo_set_line_width(cr, style.thickness) + __cairo_set_line_join(cr, style.join) +end + +return M diff --git a/src/widget/text/text.lua b/src/widget/text/text.lua new file mode 100644 index 0000000..4668e6f --- /dev/null +++ b/src/widget/text/text.lua @@ -0,0 +1,58 @@ +local M = {} + +local err = require 'err' +local dynamic = require 'dynamic' +local ti = require 'text_internal' +local source = require 'source' +local pure = require 'pure' + +-------------------------------------------------------------------------------- +-- pure + +M.config = function(font_spec, color, x_align, y_align) + return err.safe_table( + { + font_spec = font_spec, + color = color, + x_align = x_align, + y_align = y_align, + } + ) +end + +local _make = function(point, chars, config, format) + local font = ti.make_font(config.font_spec) + local get_delta_x = ti.x_align_function(config.x_align, font) + local format_chars = ti.make_format_function(format) + local make_vtext = pure.partial(ti.make_vtext, point.x, get_delta_x) + local setter = pure.memoize(pure.compose(make_vtext, format_chars)) + local static = { + y = point.y + ti.get_delta_y(config.y_align, font), + font = font, + source = source.solid_color(config.color), + } + return dynamic.single(static, setter, chars) +end + +M.make_formatted = function(point, text, config, format) + return _make(point, (text or ti.NULL_TEXT_STRING), config, format) +end + +M.make_plain = function(point, text, config) + return M.make_formatted(point, text, config, nil) +end + +-------------------------------------------------------------------------------- +-- impure + +M.set = function(obj, text) + obj.var = obj.setter(text) +end + +M.draw = function(obj, cr) + local st = obj.static + ti.set_font_spec(cr, st.font, st.source) + ti.draw_vtext_at_y(obj.var, st.y, cr) +end + +return M diff --git a/src/widget/text/text_column.lua b/src/widget/text/text_column.lua new file mode 100644 index 0000000..4686293 --- /dev/null +++ b/src/widget/text/text_column.lua @@ -0,0 +1,49 @@ +local M = {} + +local source = require 'source' +local ti = require 'text_internal' +local pure = require 'pure' +local impure = require 'impure' +local dynamic = require 'dynamic' + +-------------------------------------------------------------------------------- +-- pure + +M.make = function(point, texts, config, format, spacing) + local font = ti.make_font(config.font_spec) + local get_delta_x = ti.x_align_function(config.x_align, font) + local format_chars = ti.make_format_function(format) + local delta_y = ti.get_delta_y(config.y_align, font) + local ys = pure.map_n( + function(i) return point.y + spacing * (i - 1) + delta_y end, + #texts + ) + local make_vtext = pure.partial(ti.make_vtext, point.x, get_delta_x) + local setter = pure.memoize(pure.compose(make_vtext, format_chars)) + local static = { + y_positions = ys, + font = font, + source = source.solid_color(config.color), + } + return dynamic.multi(static, setter, texts) +end + +M.make_n = function(point, num_rows, config, format, spacing, init_text) + local dummy = pure.rep(num_rows, init_text or ti.NULL_TEXT_STRING) + return M.make(point, dummy, config, format, spacing) +end + +-------------------------------------------------------------------------------- +-- impure + +M.set = function(obj, row_num, text) + obj.var[row_num] = obj.setter(text) +end + +M.draw = function(obj, cr) + local static = obj.static + ti.set_font_spec(cr, static.font, static.source) + impure.each2(ti.draw_vtext_at_y, obj.var, static.y_positions, cr) +end + +return M diff --git a/src/widget/text/text_internal.lua b/src/widget/text/text_internal.lua new file mode 100644 index 0000000..fae08e8 --- /dev/null +++ b/src/widget/text/text_internal.lua @@ -0,0 +1,165 @@ +local M = {} + +local err = require 'err' +local geom = require 'geom' + +local __string_sub = string.sub +local __cairo_toy_font_face_create = cairo_toy_font_face_create +local __cairo_font_extents = cairo_font_extents +local __cairo_set_font_face = cairo_set_font_face +local __cairo_set_font_size = cairo_set_font_size +local __cairo_text_extents = cairo_text_extents +local __cairo_set_source = cairo_set_source +local __cairo_move_to = cairo_move_to +local __cairo_show_text = cairo_show_text + +M.NULL_TEXT_STRING = '' + +-------------------------------------------------------------------------------- +-- pure + +local trim_to_length = function(text, len) + if #text > len then + return __string_sub(text, 1, len)..'...' + else + return text + end +end + +M.make_format_function = function(format) + if type(format) == "function" then + return format + elseif type(format) == "number" and format > 0 then + return function(_text) return trim_to_length(_text, format) end + elseif type(format) == "string" then + return function(_text) return string.format(format, _text) end + elseif format == nil or format == false then + return function(_text) return _text end + else + local msg = "format must be a printf string, positive int, or function: got " + local t = type(format) + if t == "number" or t == "string" then + msg = msg..format + else + msg = msg.."a "..t + end + err.assert_trace(nil, msg) + end +end + +M.make_font_face = function(font_spec) + return __cairo_toy_font_face_create( + font_spec.family, + font_spec.slant, + font_spec.weight + ) +end + +M.make_font = function(font_spec) + return { + face = M.make_font_face(font_spec), + size = font_spec.size, + } +end + +M.make_text = function(x, y, chars) + return err.safe_table({x = x, y = y, chars = chars}) +end + +M.make_htext = function(y, delta_x, chars) + return err.safe_table({y = y, delta_x = delta_x, chars = chars}) +end + +M.make_vtext = function(x, delta_x_fun, chars) + return err.safe_table({x = x + delta_x_fun(chars), chars = chars}) +end + +-------------------------------------------------------------------------------- +-- impure + +local dummy_text_extents = cairo_text_extents_t:create() +tolua.takeownership(dummy_text_extents) + +local dummy_font_extents = cairo_font_extents_t:create() +tolua.takeownership(dummy_font_extents) + +local set_font_extents = function(font) + __cairo_set_font_size(geom.CR_DUMMY, font.size) + __cairo_set_font_face(geom.CR_DUMMY, font.face) + __cairo_font_extents(geom.CR_DUMMY, dummy_font_extents) + return dummy_font_extents +end + +local set_text_extents = function(chars, font) + __cairo_set_font_size(geom.CR_DUMMY, font.size) + __cairo_set_font_face(geom.CR_DUMMY, font.face) + __cairo_text_extents(geom.CR_DUMMY, chars, dummy_text_extents) + return dummy_text_extents +end + +M.get_width = function(chars, font) + return set_text_extents(chars, font).width +end + +M.font_height = function(font) + return set_font_extents(font).height +end + +M.x_align_function = function(x_align, font) + if x_align == 'left' then + return function(text) + local te = set_text_extents(text, font) + return -te.x_bearing + end + elseif x_align == 'center' then + return function(text) + local te = set_text_extents(text, font) + return -(te.x_bearing + te.width * 0.5) + end + elseif x_align == 'right' then + return function(text) + local te = set_text_extents(text, font) + return -(te.x_bearing + te.width) + end + else + err.assert_trace(nil, "invalid x_align") + end +end + +M.get_delta_y = function(y_align, font) + local fe = set_font_extents(font) + if y_align == 'bottom' then + return -fe.descent + elseif y_align == 'top' then + return fe.height + elseif y_align == 'center' then + return 0.92 * fe.height * 0.5 - fe.descent + else + err.assert_trace(nil, "invalid y_align") + end +end + +M.set_font_spec = function(cr, font, source) + __cairo_set_font_face(cr, font.face) + __cairo_set_font_size(cr, font.size) + __cairo_set_source(cr, source) +end + +local draw_text_at = function(cr, x, y, chars) + __cairo_move_to(cr, x, y) + __cairo_show_text(cr, chars) +end + +M.draw_text = function(obj, cr) + draw_text_at(cr, obj.x, obj.y, obj.chars) +end + +M.draw_htext_at_x = function(obj, x, cr) + draw_text_at(cr, obj.delta_x + x, obj.y, obj.chars) +end + +M.draw_vtext_at_y = function(obj, y, cr) + draw_text_at(cr, obj.x, y, obj.chars) +end + +return M diff --git a/src/widget/text/text_table.lua b/src/widget/text/text_table.lua new file mode 100644 index 0000000..32cd0f3 --- /dev/null +++ b/src/widget/text/text_table.lua @@ -0,0 +1,183 @@ +local M = {} + +local source = require 'source' +local rect = require 'rect' +local err = require 'err' +local ti = require 'text_internal' +local geom = require 'geom' +local pure = require 'pure' +local impure = require 'impure' +local style = require 'style' +local path = require 'path' +local shape = require 'shape' + +-------------------------------------------------------------------------------- +-- pure + +M.config = function(border_config, sep_config, header_config, body_config, padding) + return err.safe_table( + { + border_config = border_config, + sep_config = sep_config, + header_config = header_config, + body_config = body_config, + padding = padding, + } + ) +end + +M.body_config = function(font_spec, color, columns) + return err.safe_table( + { + font_spec = font_spec, + color = color, + columns = columns, + } + ) +end + +M.column_config = function(header, format) + return err.safe_table({header = header, format = format}) +end + +M.header_config = function(font_spec, color, bottom_padding) + return err.safe_table( + { + font_spec = font_spec, + color = color, + bottom_padding = bottom_padding, + } + ) +end + +M.padding = function(l, t, r, b) + return err.safe_table( + { + left = l, + right = r, + top = t, + bottom = b, + } + ) +end + +-- TODO this is basically the same as that from ylabels +local make_header_text = function(x, y, chars, font) + return ti.make_text( + x + ti.x_align_function('center', font)(chars), + y + ti.get_delta_y('center', font), + chars + ) +end + +-- M.make = function(box, num_rows, columns, table_config) +M.make = function(box, num_rows, table_config) + local bc = table_config.body_config + local hs = table_config.header_config + local columns = bc.columns + local header_font = ti.make_font(hs.font_spec) + local body_font = ti.make_font(bc.font_spec) + local p = table_config.padding + local tbl_width = box.width - p.left - p.right + local tbl_height = box.height - p.top - p.bottom + local tbl_x = box.corner.x + p.left + local tbl_y = box.corner.y + p.top + local body_delta_y = ti.get_delta_y('center', body_font) + local body_y = tbl_y + hs.bottom_padding + body_delta_y + local column_width = tbl_width / #columns + local spacing = (tbl_height - hs.bottom_padding) / (num_rows - 1) + local headers = pure.imap( + function (i, conf) + local x = tbl_x + column_width * (i - 0.5) + return make_header_text(x, tbl_y, conf.header, header_font) + end, + columns + ) + local sep_paths = pure.map_n( + function(i) + local x = tbl_x + column_width * i + return path.create_line( + geom.CR_DUMMY, + geom.make_point(x, tbl_y), + geom.make_point(x, tbl_y + tbl_height) + ) + end, + #columns - 1 + ) + + local get_delta_x = ti.x_align_function('center', body_font) + local make_setter = function(i, conf) + local column_x = tbl_x + column_width * (i - 0.5) + return pure.memoize( + pure.compose( + pure.partial(ti.make_vtext, column_x, get_delta_x), + ti.make_format_function(conf.format) + ) + ) + end + local setters = pure.imap(make_setter, columns) + + return { + static = { + header = { + font = header_font, + source = source.solid_color(hs.color), + texts = headers, + }, + body = { + font = body_font, + source = source.solid_color(bc.color), + y_positions = pure.map_n( + function(i) return body_y + spacing * (i - 1) end, + num_rows + ), + }, + separators = { + style = table_config.sep_config.style, + shapes = { + source = source.solid_color(table_config.sep_config.pattern), + paths = sep_paths, + }, + }, + border = rect.make(box, table_config.border_config), + }, + setters = setters, + var = pure.map_n( + function(c) return pure.rep(num_rows, setters[c]("NULL")) end, + #columns + ) + } +end + +-------------------------------------------------------------------------------- +-- impure + +M.set = function(obj, col_num, row_num, text) + obj.var[col_num][row_num] = obj.setters[col_num](text) +end + +M.draw_static = function(obj, cr) + local static = obj.static + local seps = static.separators + local header = static.header + + rect.draw(static.border, cr) + + style.set_line_style(seps.style, cr) + shape.draw_shapes(seps.shapes, cr) + + ti.set_font_spec(cr, header.font, header.source) + impure.each(ti.draw_text, header.texts, cr) +end + +local draw_column = function(rows, ys, cr) + impure.each2(ti.draw_vtext_at_y, rows, ys, cr) +end + +M.draw_dynamic = function(obj, cr) + local body = obj.static.body + ti.set_font_spec(cr, body.font, body.source) + impure.each(draw_column, obj.var, body.y_positions, cr) +end + +return M diff --git a/src/widget/text/text_threshold.lua b/src/widget/text/text_threshold.lua new file mode 100644 index 0000000..6acbb2b --- /dev/null +++ b/src/widget/text/text_threshold.lua @@ -0,0 +1,79 @@ +local M = {} + +local err = require 'err' +local pure = require 'pure' +local source = require 'source' +local ti = require 'text_internal' +local dynamic = require 'dynamic' + +local __tonumber = tonumber + +-------------------------------------------------------------------------------- +-- pure + +local _make = function(point, chars, config, threshold_config, format) + local font = ti.make_font(config.font_spec) + local get_delta_x = ti.x_align_function(config.x_align, font) + local make_vtext = pure.partial(ti.make_vtext, point.x, get_delta_x) + local format_chars = ti.make_format_function(format) + local source_chooser = source.solid_color_chooser( + config.color, + threshold_config.high_color, + threshold_config.threshold + ) + local _setter = pure.memoize( + function(cs) + local _cs = cs or 0 + return { + text = make_vtext(format_chars(_cs)), + source = source_chooser(_cs), + } + end + ) + local f = threshold_config.pre_function + local setter + if f then + setter = function(x) return _setter(f(x)) end + else + setter = _setter + end + local static = { + y = point.y + ti.get_delta_y(config.y_align, font), + font = font, + } + return dynamic.single(static, setter, chars or 0) +end + +M.config = function(high_color, threshold, pre_function) + return err.safe_table( + { + high_color = high_color, + threshold = threshold, + pre_function = pre_function, + } + ) +end + +M.make_formatted = function(point, text, config, format, threshold_config) + return _make(point, text, config, threshold_config, format) +end + +M.make_plain = function(point, text, config, threshold_config) + return M.make_formatted(point, text, config, threshold_config, nil) +end + +-------------------------------------------------------------------------------- +-- impure + +M.set = function(obj, x) + obj.var = obj.setter(x) +end + +M.draw = function(obj, cr) + local st = obj.static + local var = obj.var + ti.set_font_spec(cr, st.font, var.source) + ti.draw_vtext_at_y(var.text, st.y, cr) +end + +return M diff --git a/src/widget/timeseries/scaled_timeseries.lua b/src/widget/timeseries/scaled_timeseries.lua new file mode 100644 index 0000000..2bf8442 --- /dev/null +++ b/src/widget/timeseries/scaled_timeseries.lua @@ -0,0 +1,269 @@ +local M = {} + +local timeseries = require 'timeseries' +local tsi = require 'timeseries_internal' +local ti = require 'text_internal' +local err = require 'err' +local ylabels = require 'ylabels' +local xlabels = require 'xlabels' +local geom = require 'geom' +local source = require 'source' +local pure = require 'pure' +local impure = require 'impure' + +local __table_remove = table.remove +local __math_ceil = math.ceil +local __math_log = math.log + +-------------------------------------------------------------------------------- +-- pure + +local choose_scale_factor = function(timers, new_sf) + if #timers == 0 then + return new_sf + else + local cur_sf = timers[1].factor + if new_sf < cur_sf then + return new_sf + else + return cur_sf + end + end +end + +M.scaling_parameters = function(base, min_domain, threshold) + return err.safe_table( + { + base = base, + min_domain = min_domain, + threshold = threshold, + } + ) +end + +local tick_timer = function(timer) + timer.remaining = timer.remaining - 1 +end + +-- ASSUME +-- 1. this is a FIFO queue +-- 2. timers will be sorted from highest scale to lowest scale going from +-- back to front +-- 3. no timers will share a time slot +-- 4. no timers will have the same scale factor +-- 5. the table will always be a sequence +-- NOTE: scale factor is inversely related to scale, so higher scale -> lower +-- factor (hence the inequalities) +local update_timers = function(timers, prev_sf, new_sf, init_timer) + if timers[1] and timers[1].remaining == 0 then + __table_remove(timers, 1) + end + + impure.each(tick_timer, timers) + local n = #timers + + if new_sf < prev_sf then + while n > 0 and timers[n].factor >= new_sf do + timers[n] = nil + n = n - 1 + end + elseif new_sf > prev_sf and (n == 0 or prev_sf > timers[1].factor) then + timers[n + 1] = init_timer(prev_sf) + end + return timers +end + +local scale_point = function(value, y, h, new_factor, old_factor) + return y + h * (1 - (1 - (value - y) / h) * (new_factor / old_factor)) +end + +-- local debug_timers = function(timers, sf, value) +-- print('----------------------------------------------------------------------') +-- print('value', value, 'scale_factor', sf) +-- for i, v in pairs(timers) do +-- print('timers', 'i', i, 't', v.remaining, 'f', v.factor) +-- end +-- print('length', #timers) +-- end + +M.make = function(box, samplefreq, plot_config, label_config, scaling_params) + local t = scaling_params.threshold + local m = scaling_params.min_domain + local b = scaling_params.base + + local get_scale_factor = function(x) + local domain = m + if x > 0 then + domain = __math_ceil(__math_log(x / t) / __math_log(b)) + end + if domain < m then domain = m end + return b ^ -domain + end + + local x = box.corner.x + local gconf = plot_config.grid_config + local x_label_format = timeseries.make_format_timecourse_x_label(plot_config.num_points, samplefreq) + local label_font = ti.make_font(label_config.font_spec) + local x_label_data = xlabels.make(box.bottom_y, gconf.num_x + 1, x_label_format, label_font) + local axis_x = box.corner.x + local right_x = axis_x + box.width + local plot_y = box.corner.y + local total_width = box.width + local n_y_labels = gconf.num_y + 1 + + local plot_height = box.height - xlabels.get_x_axis_height(label_font) + + local make_y_labels = pure.memoize( + function(scale_factor) + return ylabels.make( + box.corner, + plot_height, + n_y_labels, + label_font, + label_config.y_format, + scale_factor + ) + end + ) + + local make_grid_paths = pure.memoize( + function(y_axis_width) + return tsi.make_plotarea_paths( + geom.CR_DUMMY, + right_x, + plot_y, + total_width - y_axis_width, + plot_height, + gconf.num_x, + gconf.num_y + ) + end + ) + + local get_x_label_positions = pure.memoize( + function(y_axis_width) + local w = total_width - y_axis_width + return xlabels.get_x_label_positions(right_x, w, x_label_data) + end + ) + + local init_timer = function(scale_factor) + return { + factor = scale_factor, + remaining = plot_config.num_points + } + end + + local scale_data_points = function(series, old_factor, new_factor) + if old_factor == new_factor then + return series + else + return pure.map(scale_point, series, plot_y, plot_height, new_factor, old_factor) + end + end + + local insert_data_point = function(series, value, scale_factor) + return tsi.insert_data_point(plot_y, plot_height, plot_config.num_points, series, value * scale_factor) + end + + local inner_setter = function(value, series, cur_sf, prev_sf, timers) + local new_sf = get_scale_factor(value) + local new_timers = update_timers(timers, prev_sf, new_sf, init_timer) + local new_cur_sf = choose_scale_factor(new_timers, new_sf) + local y_labels = make_y_labels(1 / new_cur_sf) + local width = total_width - y_labels.width + -- debug_timers(new_timers, new_sf, value) + return { + axis = { + x = { + positions = get_x_label_positions(y_labels.width), + }, + y = { + labels = y_labels, + } + }, + current_scale_factor = new_cur_sf, + prev_scale_factor = new_sf, + plotarea = { + dx = width / plot_config.num_points, + paths = make_grid_paths(y_labels.width) + }, + timers = new_timers, + series = insert_data_point( + scale_data_points(series, cur_sf, new_cur_sf), + value, + new_cur_sf + ) + } + end + + local setter = function(value, var) + return inner_setter( + value, + var.series, + var.current_scale_factor, + var.prev_scale_factor, + var.timers + ) + end + + return err.safe_table( + { + static = { + box = box, + axis = { + font = label_font, + source = source.solid_color(label_config.color), + x = { + label_data = x_label_data, + } + }, + plotarea = { + bottom_y = plot_height + plot_y, + sources = tsi.make_sources(x, plot_y, total_width, plot_config), + }, + }, + setter = setter, + var = inner_setter(0, {}, 1, 1, {}), + } + ) +end + +-------------------------------------------------------------------------------- +-- impure + +M.update = function(obj, value) + obj.var = obj.setter(value, obj.var) +end + +-- nothing here is "static" because we cannot assume that +-- any object will remain the same shape (can shift in both x and y) +M.draw_static = function(_, _) + -- stub +end + +M.draw_dynamic = function(obj, cr) + local var = obj.var + local static = obj.static + local box = static.box + local saxis = static.axis + local vaxis = var.axis + local vplotarea = var.plotarea + local splotarea = static.plotarea + local sources = splotarea.sources + local paths = vplotarea.paths + local right_x = box.right_x + timeseries.draw_labels( + cr, + saxis.font, + saxis.source, + vaxis.x.positions, + saxis.x.label_data, + vaxis.y.labels + ) + tsi.draw_grid(cr, paths.grid, sources.grid) + tsi.draw_series(cr, right_x, splotarea.bottom_y, vplotarea.dx, var.series, sources.series) + tsi.draw_outline(cr, paths.outline, sources.outline) +end + +return M diff --git a/src/widget/timeseries/timeseries.lua b/src/widget/timeseries/timeseries.lua new file mode 100644 index 0000000..3732c06 --- /dev/null +++ b/src/widget/timeseries/timeseries.lua @@ -0,0 +1,163 @@ +local M = {} + +local err = require 'err' +local xlabels = require 'xlabels' +local ylabels = require 'ylabels' +local source = require 'source' +local ti = require 'text_internal' +local tsi = require 'timeseries_internal' +local geom = require 'geom' +local impure = require 'impure' + +local __string_format = string.format + +-------------------------------------------------------------------------------- +-- pure + +-- TODO group these better +M.config = function(num_points, outline_color, data_line_pattern, + data_fill_pattern, grid_config) + return err.safe_table( + { + num_points = num_points, + outline_color = outline_color, + data_line_pattern = data_line_pattern, + data_fill_pattern = data_fill_pattern, + grid_config = grid_config, + } + ) +end + +M.grid_config = function(num_x, num_y, color) + return err.safe_table( + { + num_x = num_x, + num_y = num_y, + pattern = color, + } + ) +end + +M.label_config = function(color, font_spec, y_format) + return err.safe_table( + { + color = color, + font_spec = font_spec, + y_format = y_format + } + ) +end + +M.make_format_timecourse_x_label = function(n, freq) + return function(x) return __string_format('%.0fs', (1 - x) * n / freq) end +end + +M.make = function(box, samplefreq, config, label_config) + local x = box.corner.x + local y = box.corner.y + local w = box.width + local h = box.height + local right_x = x + w + local bottom_y = y + h + local gconf = config.grid_config + local label_font = ti.make_font(label_config.font_spec) + + local plot_height = box.height - xlabels.get_x_axis_height(label_font) + local y_labels = ylabels.make( + box.corner, + plot_height, + gconf.num_y + 1, + label_font, + label_config.y_format, + 1 + ) + + local plot_width = w - y_labels.width + + local x_label_format = M.make_format_timecourse_x_label(config.num_points, samplefreq) + + local x_labels = xlabels.make(bottom_y, gconf.num_x + 1, x_label_format, label_font) + + local setter = function(value, series) + return tsi.insert_data_point(y, plot_height, config.num_points, series, value) + end + + return err.safe_table( + { + static = { + box = box, + axis = { + font = label_font, + source = source.solid_color(label_config.color), + x = { + label_data = x_labels, + positions = xlabels.get_x_label_positions(right_x, plot_width, x_labels), + }, + y = { + labels = y_labels, + } + }, + plotarea = { + dx = plot_width / config.num_points, + bottom_y = y + plot_height, + paths = tsi.make_plotarea_paths( + geom.CR_DUMMY, + right_x, + y, + plot_width, + plot_height, + gconf.num_x, + gconf.num_y + ), + sources = tsi.make_sources(x, y, w, config), + }, + }, + setter = setter, + var = setter(0, {}), + } + ) +end + +-------------------------------------------------------------------------------- +-- impure + +M.update = function(obj, value) + obj.var = obj.setter(value, obj.var) +end + +M.draw_labels = function(cr, font, _source, x_positions, x_label_data, y_labels) + ti.set_font_spec(cr, font, _source) + impure.each2(ti.draw_htext_at_x, x_label_data, x_positions, cr) + impure.each(ti.draw_text, y_labels, cr) +end + + +M.draw_static = function(obj, cr) + local static = obj.static + local axis = static.axis + local ax = axis.x + local plotarea = static.plotarea + local paths = plotarea.paths + local sources = plotarea.sources + M.draw_labels( + cr, + axis.font, + axis.source, + ax.positions, + ax.label_data, + axis.y.labels + ) + tsi.draw_grid(cr, paths.grid, sources.grid) +end + +M.draw_dynamic = function(obj, cr) + local static = obj.static + local box = static.box + local plotarea = static.plotarea + local right_x = box.right_x + local sources = plotarea.sources + tsi.draw_series(cr, right_x, plotarea.bottom_y, plotarea.dx, obj.var, sources.series) + tsi.draw_outline(cr, plotarea.paths.outline, sources.outline) +end + +return M diff --git a/src/widget/timeseries/timeseries_internal.lua b/src/widget/timeseries/timeseries_internal.lua new file mode 100644 index 0000000..256875b --- /dev/null +++ b/src/widget/timeseries/timeseries_internal.lua @@ -0,0 +1,132 @@ +local M = {} + +local source = require 'source' +local geom = require 'geom' +local pure = require 'pure' +local style = require 'style' +local path = require 'path' +local shape = require 'shape' + +local __cairo_move_to = cairo_move_to +local __cairo_line_to = cairo_line_to +local __cairo_set_source = cairo_set_source +local __cairo_fill_preserve = cairo_fill_preserve +local __cairo_stroke = cairo_stroke +local __table_insert = table.insert + +local DATA_STYLE = style.open_poly(1, CAIRO_LINE_CAP_BUTT, CAIRO_LINE_JOIN_MITER) +local GRID_CONFIG = style.line(1, CAIRO_LINE_CAP_BUTT) +local OUTLINE_STYLE = style.open_poly(2, CAIRO_LINE_CAP_BUTT, CAIRO_LINE_JOIN_MITER) + +-------------------------------------------------------------------------------- +-- pure + +local make_x_grid = function(cr, x, y, w, h, n) + local y1 = y - 0.5 + local y2 = y1 + h + 0.5 + local grid_line_spacing = w / n + local f = function(i) + local x1 = x - w + grid_line_spacing * i - 0.5 + local p1 = geom.make_point(x1, y1) + local p2 = geom.make_point(x1, y2) + return path.create_line(cr, p1, p2) + end + return pure.map_n(f, n) +end + +local make_y_grid = function(cr, x, y, w, h, n) + local x1 = x + local x2 = x - w + local grid_line_spacing = h / n + local f = function(i) + local y1 = y + (i - 1) * grid_line_spacing - 0.5 + local p1 = geom.make_point(x1, y1) + local p2 = geom.make_point(x2, y1) + return path.create_line(cr, p1, p2) + end + return pure.map_n(f, n) +end + +local make_grid_paths = function(cr, x, y, w, h, nx, ny) + return { + x = make_x_grid(cr, x, y, w, h, nx), + y = make_y_grid(cr, x, y, w, h, ny), + } +end + +local make_outline = function(cr, right_x, y, w, h) + local x1 = right_x - w + local y1 = y - 0.5 + local x2 = right_x + 0.5 + local y2 = y + h + 1.0 + local p1 = geom.make_point(x1, y1) + local p2 = geom.make_point(x1, y2) + local p3 = geom.make_point(x2, y2) + return path.create_open_poly(cr, {p1, p2, p3}) +end + +M.make_plotarea_paths = function(cr, right_x, y, w, h, num_x, num_y) + return { + grid = make_grid_paths(cr, right_x, y, w, h, num_x, num_y), + outline = make_outline(cr, right_x, y, w, h), + } +end + +M.make_sources = function(x, y, width, config) + local p1 = geom.make_point(x, y) + local p2 = geom.make_point(x + width, y) + return { + grid = source.solid_color(config.grid_config.pattern), + outline = source.solid_color(config.outline_color), + series = { + line = source.linear_pattern(config.data_line_pattern, p1, p2), + fill = source.linear_pattern(config.data_fill_pattern, p1, p2), + } + } +end + +-------------------------------------------------------------------------------- +-- impure + +M.insert_data_point = function(y, h, n, series, value) + __table_insert(series, 1, y + h * (1 - value)) + if #series == n + 2 then + series[#series] = nil + end + return series +end + +M.draw_grid = function(cr, grid, _source) + style.set_line_style(GRID_CONFIG, cr) + -- TODO this sets the same source twice and strokes twice + shape.draw_paths_with_source(grid.x, cr, _source) + shape.draw_paths_with_source(grid.y, cr, _source) +end + +M.draw_outline = function(cr, _path, _source) + style.set_open_poly_style(OUTLINE_STYLE, cr) + shape.draw_path_with_source(_path, cr, _source) +end + +M.draw_series = function(cr, right_x, bottom_y, dx, series, sources) + style.set_open_poly_style(DATA_STYLE, cr) + local n = #series + + -- TODO this accounts for up to 20% of the execution time once the series + -- reaches its max length + __cairo_move_to(cr, right_x, series[1]) + + for j = 2, n do + __cairo_line_to(cr, right_x - (j - 1) * dx, series[j]) + end + + __cairo_line_to(cr, right_x - (n - 1) * dx, bottom_y) + __cairo_line_to(cr, right_x, bottom_y) + __cairo_set_source(cr, sources.fill) + __cairo_fill_preserve(cr) + + __cairo_set_source(cr, sources.line) + __cairo_stroke(cr) +end + +return M diff --git a/src/widget/timeseries/xlabels.lua b/src/widget/timeseries/xlabels.lua new file mode 100644 index 0000000..86657ef --- /dev/null +++ b/src/widget/timeseries/xlabels.lua @@ -0,0 +1,46 @@ +local M = {} + +local ti = require 'text_internal' +local pure = require 'pure' + +-------------------------------------------------------------------------------- +-- pure + +local make_x_label_text = function(y, chars, font) + return ti.make_htext( + y + ti.get_delta_y('bottom', font), + ti.x_align_function('center', font)(chars), + chars + ) +end + +local X_LABEL_PAD = 8 + +M.get_x_axis_height = function(font) + return ti.font_height(font) + X_LABEL_PAD +end + +M.make = function(y, n, format_fun, font) + local f = function(i) + return make_x_label_text(y, format_fun((i - 1) / (n - 1)), font) + end + return pure.map_n(f, n) +end + +M.get_x_label_positions = function(right_x, w, x_labels) + local n = #x_labels + local f = function(i) + return right_x - w * (1 - (i - 1) / (n - 1)) + end + return pure.map_n(f, n) +end + +-------------------------------------------------------------------------------- +-- impure + +M.draw = function(obj, cr, positions) + ti.set_font_spec(cr, obj.font, obj.source) + pure.each2(ti.draw_htext_at, obj.labels, positions, cr) +end + +return M diff --git a/src/widget/timeseries/ylabels.lua b/src/widget/timeseries/ylabels.lua new file mode 100644 index 0000000..be076fc --- /dev/null +++ b/src/widget/timeseries/ylabels.lua @@ -0,0 +1,47 @@ +local M = {} + +local geom = require 'geom' +local text = require 'text' +local ti = require 'text_internal' + +local Y_LABEL_PAD = 5 + +-------------------------------------------------------------------------------- +-- pure + +local make_y_label_text = function(point, chars, font) + return ti.make_text( + point.x, + point.y + ti.get_delta_y('center', font), + chars + ) +end + + +-- TODO this function smells funny +M.make = function(point, h, n, font, y_format, scale_factor) + local y_labels = {width = 0} + local f = y_format(scale_factor) + for i = 1, n do + local z = (i - 1) / (n - 1) + local l = make_y_label_text( + geom.make_point(point.x, point.y + z * h), + f((1 - z) * scale_factor), + font + ) + local w = ti.get_width(l.chars, font) + if w > y_labels.width then + y_labels.width = w + end + y_labels[i] = l + end + y_labels.width = y_labels.width + Y_LABEL_PAD + return y_labels +end + +-------------------------------------------------------------------------------- +-- impure + +M.draw = text.draw + +return M