From c53f044bda69aa1820640789b4fb894a9151a22b Mon Sep 17 00:00:00 2001 From: TiagoLr Date: Sat, 16 Sep 2023 08:27:04 +0100 Subject: [PATCH] Release Snapshooter v1.0 (#1248) --- Envelopes/tilr_Snapshooter.lua | 637 ++ Envelopes/tilr_Snapshooter/rtk.lua | 5686 +++++++++++++++++ .../tilr_Snapshooter apply snap 1.lua | 12 + .../tilr_Snapshooter apply snap 2.lua | 12 + .../tilr_Snapshooter apply snap 3.lua | 12 + .../tilr_Snapshooter apply snap 4.lua | 12 + .../tilr_Snapshooter save snap 1.lua | 8 + .../tilr_Snapshooter save snap 2.lua | 8 + .../tilr_Snapshooter save snap 3.lua | 8 + .../tilr_Snapshooter save snap 4.lua | 8 + .../tilr_Snapshooter write snap 1.lua | 12 + .../tilr_Snapshooter write snap 2.lua | 12 + .../tilr_Snapshooter write snap 3.lua | 12 + .../tilr_Snapshooter write snap 4.lua | 12 + 14 files changed, 6451 insertions(+) create mode 100644 Envelopes/tilr_Snapshooter.lua create mode 100644 Envelopes/tilr_Snapshooter/rtk.lua create mode 100644 Envelopes/tilr_Snapshooter/tilr_Snapshooter apply snap 1.lua create mode 100644 Envelopes/tilr_Snapshooter/tilr_Snapshooter apply snap 2.lua create mode 100644 Envelopes/tilr_Snapshooter/tilr_Snapshooter apply snap 3.lua create mode 100644 Envelopes/tilr_Snapshooter/tilr_Snapshooter apply snap 4.lua create mode 100644 Envelopes/tilr_Snapshooter/tilr_Snapshooter save snap 1.lua create mode 100644 Envelopes/tilr_Snapshooter/tilr_Snapshooter save snap 2.lua create mode 100644 Envelopes/tilr_Snapshooter/tilr_Snapshooter save snap 3.lua create mode 100644 Envelopes/tilr_Snapshooter/tilr_Snapshooter save snap 4.lua create mode 100644 Envelopes/tilr_Snapshooter/tilr_Snapshooter write snap 1.lua create mode 100644 Envelopes/tilr_Snapshooter/tilr_Snapshooter write snap 2.lua create mode 100644 Envelopes/tilr_Snapshooter/tilr_Snapshooter write snap 3.lua create mode 100644 Envelopes/tilr_Snapshooter/tilr_Snapshooter write snap 4.lua diff --git a/Envelopes/tilr_Snapshooter.lua b/Envelopes/tilr_Snapshooter.lua new file mode 100644 index 000000000..1d14d58b0 --- /dev/null +++ b/Envelopes/tilr_Snapshooter.lua @@ -0,0 +1,637 @@ +-- @description Snapshooter +-- @author tilr +-- @version 1.0 +-- @provides +-- tilr_Snapshooter/rtk.lua +-- [main] tilr_Snapshooter/tilr_Snapshooter apply snap 1.lua +-- [main] tilr_Snapshooter/tilr_Snapshooter apply snap 2.lua +-- [main] tilr_Snapshooter/tilr_Snapshooter apply snap 3.lua +-- [main] tilr_Snapshooter/tilr_Snapshooter apply snap 4.lua +-- [main] tilr_Snapshooter/tilr_Snapshooter save snap 1.lua +-- [main] tilr_Snapshooter/tilr_Snapshooter save snap 2.lua +-- [main] tilr_Snapshooter/tilr_Snapshooter save snap 3.lua +-- [main] tilr_Snapshooter/tilr_Snapshooter save snap 4.lua +-- [main] tilr_Snapshooter/tilr_Snapshooter write snap 1.lua +-- [main] tilr_Snapshooter/tilr_Snapshooter write snap 2.lua +-- [main] tilr_Snapshooter/tilr_Snapshooter write snap 3.lua +-- [main] tilr_Snapshooter/tilr_Snapshooter write snap 4.lua +-- @screenshot https://raw.githubusercontent.com/tiagolr/snapshooter/master/doc/snapshooter.gif +-- @about +-- # Snapshooter +-- +-- https://github.com/tiagolr/snapshooter +-- +-- Snapshooter allows to create param snapshots and recall or write them to the playlist creating patch morphs. +-- Different from SWS snapshots, only the params changed are written to the playlist as automation points. +-- +-- Features: +-- * Stores and retrieves FX params +-- * Stores and retrieves mixer states for track Volume, Pan, Mute and Sends +-- * Writes only changed params by diffing the current state and selected snapshot +-- * Transition snapshots using tween and ease functions +-- * Tested with hundreds of params with minimal overhead +-- +-- Tips: +-- * Set global automation to _READ_ to save current song snapshot +-- * Set global automation to other value than _READ_ to save snapshots from mixer state +-- * If params are not writing make sure they have a different current value from the snapshot + +function log(t) + reaper.ShowConsoleMsg(t .. '\n') +end +function logtable(table, indent) + log(tostring(table)) + for index, value in pairs(table) do -- print table + log(' ' .. tostring(index) .. ' : ' .. tostring(value)) + end +end + +globals = { + ui_checkbox_seltracks = false, + ui_checkbox_volume = true, + ui_checkbox_pan = true, + ui_checkbox_mute = true, + ui_checkbox_sends = true, + tween = 'none', + ease = 'linear' +} + +-- init globals from project config +local exists, seltracks = reaper.GetProjExtState(0, 'snapshooter', 'ui_checkbox_seltracks') +if exists ~= 0 then globals.ui_checkbox_seltracks = seltracks == 'true' end +local exists, volume = reaper.GetProjExtState(0, 'snapshooter', 'ui_checkbox_volume') +if exists ~= 0 then globals.ui_checkbox_volume = volume == 'true' end +local exists, pan = reaper.GetProjExtState(0, 'snapshooter', 'ui_checkbox_pan') +if exists ~= 0 then globals.ui_checkbox_pan = pan == 'true' end +local exists, mute = reaper.GetProjExtState(0, 'snapshooter', 'ui_checkbox_mute') +if exists ~= 0 then globals.ui_checkbox_mute = mute == 'true' end +local exists, sends = reaper.GetProjExtState(0, 'snapshooter', 'ui_checkbox_sends') +if exists ~= 0 then globals.ui_checkbox_sends = sends == 'true' end +local exists, tween = reaper.GetProjExtState(0, 'snapshooter', 'ui_tween_menu') +if exists ~= 0 then globals.tween = tween end +local exists, ease = reaper.GetProjExtState(0, 'snapshooter', 'ui_ease_menu') +if exists ~= 0 then globals.ease = ease end + +function makesnap() + local numtracks = reaper.GetNumTracks() + local entries = {} + for i = 0, numtracks - 1 do + -- vol,send,mute + local tr = reaper.GetTrack(0, i) + local ret, vol, pan = reaper.GetTrackUIVolPan(tr) + local ret, mute = reaper.GetTrackUIMute(tr) + table.insert(entries, { tr, 'Volume', vol }) + table.insert(entries, { tr, 'Pan', pan }) + table.insert(entries, { tr, 'Mute', mute and 1 or 0 }) + -- sends + local sendcount = {} -- aux send counter + for send = 0, reaper.GetTrackNumSends(tr, 0) do + local src = reaper.GetTrackSendInfo_Value(tr, 0, send, 'P_SRCTRACK') + local dst = reaper.GetTrackSendInfo_Value(tr, 0, send, 'P_DESTTRACK') + local ret, svol, span = reaper.GetTrackSendUIVolPan(tr, send) + local ret, smute = reaper.GetTrackSendUIMute(tr, send) + key = tostring(src)..tostring(dst) + count = sendcount[key] and sendcount[key] + 1 or 1 + table.insert(entries, { tr, 'Send', count, key, svol, span, smute and 1 or 0 }) + sendcount[key] = count + end + -- fx params + local fxcount = reaper.TrackFX_GetCount(tr) + for j = 0, fxcount - 1 do + fxid = reaper.TrackFX_GetFXGUID(tr, j) + paramcount = reaper.TrackFX_GetNumParams(tr, j) + for k = 0, paramcount - 1 do + param = reaper.TrackFX_GetParam(tr, j, k) + table.insert(entries, { tr, fxid , k, param }) + end + end + end + return entries +end + +function difference (s1, s2) + diff = {} + hashmap = {} + -- create snap2 hashmap + for i, line2 in pairs(s2) do + if #line2 == 3 then + hashmap[line2[1]..line2[2]] = line2 + elseif #line2 == 4 then -- fx param + hashmap[line2[1]..line2[2]..line2[3]] = line2 + elseif #line2 == 7 then -- sends + hashmap[line2[1]..line2[2]..line2[3]..line2[4]] = line2 + end + end + -- compare s1 with s2 entries + for i, line1 in pairs(s1) do + if #line1 == 3 then -- volpanmute + local line2 = hashmap[line1[1]..line1[2]] + if line2 and line1[3] ~= line2[3] then + table.insert(diff, line1) + end + elseif #line1 == 4 then -- params + local line2 = hashmap[line1[1]..line1[2]..line1[3]] + if line2 and line1[3] ~= line2[3] then + table.insert(diff, line1) + elseif line2 and line1[4] ~= line2[4] then + table.insert(diff, line1) + end + elseif #line1 == 7 then -- sends + local line2 = hashmap[line1[1]..line1[2]..line1[3]..line1[4]] + if line2 and (line1[5] ~= line2[5] or line1[6] ~= line2[6] or line1[7] ~= line2[7]) then + if line1[5] == line2[5] then line1[5] = 'unchanged' end + if line1[6] == line2[6] then line1[6] = 'unchanged' end + if line1[7] == line2[7] then line1[7] = 'unchanged' end + table.insert(diff, line1) + end + end + end + return diff +end + +-- stringify/parse snapshot for mem storage +function stringify(snap) + local entries = {} + for i, line in ipairs(snap) do + for j, col in ipairs(line) do + line[j] = tostring(col) + end + table.insert(entries, table.concat(line, ',')) + end + return table.concat(entries, '\n') +end + +function parse(snapstr) + function splitline(line) + local split = {} + for word in string.gmatch(line, '([^,]+)') do + table.insert(split, word) + end + return split + end + local lines = {} + for line in string.gmatch(snapstr, '([^\n]+)') do + table.insert(lines, splitline(line)) + end + return lines +end + +function insertSendEnvelopePoint(track, key, count, value, type) + local count = 0 + local cursor = reaper.GetCursorPosition() + for send = 0, reaper.GetTrackNumSends(track, 0) do + local src = reaper.GetTrackSendInfo_Value(track, 0, send, 'P_SRCTRACK') + local dst = reaper.GetTrackSendInfo_Value(track, 0, send, 'P_DESTTRACK') + if tostring(src)..tostring(dst) == key then + count = count + 1 + if count == cnt then + local envelope = reaper.GetTrackSendInfo_Value(track, 0, send, 'P_ENV:<'..type) + local scaling = reaper.GetEnvelopeScalingMode(envelope) + reaper.InsertEnvelopePoint(envelope, cursor, reaper.ScaleToEnvelopeMode(scaling, value), 0, 0, true) + br_env = reaper.BR_EnvAlloc(envelope, false) + active, visible, armed, inLane, laneHeight, defaultShape, minValue, maxValue, centerValue, type, faderScaling = reaper.BR_EnvGetProperties(br_env) + reaper.BR_EnvSetProperties(br_env, true, true, true, inLane, laneHeight, defaultShape, faderScaling) + reaper.BR_EnvFree(br_env, 1) + end + end + end +end + +-- show Volume/Pan/Mute envelopes for track +function showTrackEnvelopes(track, envtype) + if envtype == 'Volume' then + if not reaper.GetTrackEnvelopeByName(track, 'Volume') then + reaper.SetOnlyTrackSelected(track) + reaper.Main_OnCommand(40406, 0) -- Set track volume visible + end + elseif envtype == 'Pan' then + if not reaper.GetTrackEnvelopeByName(track, 'Pan') then + reaper.SetOnlyTrackSelected(track) + reaper.Main_OnCommand(40407, 0) -- Set track pan visible + end + elseif envtype == 'Mute' then + if not reaper.GetTrackEnvelopeByName(track, 'Mute') then + reaper.SetOnlyTrackSelected(track) + reaper.Main_OnCommand(40867, 0) -- Set track mute visible + end + end +end + +-- apply snapshot to params or write to timeline +function applydiff(diff, write, tween) + local numtracks = reaper.GetNumTracks() + local cursor = reaper.GetCursorPosition() + -- tracks hashmap + local tracks = {} + for i = 0, numtracks - 1 do + tracks[tostring(reaper.GetTrack(0, i))] = i + end + -- fxs hashmap + local fxs = {} + for guid, tr in pairs(tracks) do + track = reaper.GetTrack(0, tr) + fxcount = reaper.TrackFX_GetCount(track) + for j = 0, fxcount - 1 do + fxs[reaper.TrackFX_GetFXGUID(track, j)] = j + end + end + -- sends hashmap + local sends = {} + for i = 0, numtracks - 1 do + track = reaper.GetTrack(0, i) + local sendscount = {} + for i = 0, reaper.GetTrackNumSends(track, 0) do + local src = reaper.GetTrackSendInfo_Value(track, 0, i, 'P_SRCTRACK') + local dst = reaper.GetTrackSendInfo_Value(track, 0, i, 'P_DESTTRACK') + key = tostring(src)..tostring(dst) + count = sendscount[key] and sendscount[key] + 1 or 1 + sendscount[key] = count + sends[key..tostring(count)] = i + end + end + + -- apply diff lines + for i,line in ipairs(diff) do + tr = tracks[tostring(line[1])] + if tr ~= nil then + local track = reaper.GetTrack(0, tr) + if not globals.ui_checkbox_seltracks or reaper.IsTrackSelected(track) then + if #line == 3 then -- vol/pan/mute + param = line[2] + value = tonumber(line[3]) + if param == 'Volume' and globals.ui_checkbox_volume or + param == 'Pan' and globals.ui_checkbox_pan or + param == 'Mute' and globals.ui_checkbox_mute then + if write then + showTrackEnvelopes(track, line[2]) + env = reaper.GetTrackEnvelopeByName(track, param) + if param == 'Volume' then + local scaling = reaper.GetEnvelopeScalingMode(env) + reaper.InsertEnvelopePoint(env, cursor, reaper.ScaleToEnvelopeMode(scaling, value), 0, 0, true) + else + -- FIX flip mute value before writting to playlist + if param == 'Mute' and value == 1 then value = 0 + elseif param == 'Mute' and value == 0 then value = 1 end + -- FIX flip pan value before writting to playlist + if param == 'Pan' then value = -value end + -- + reaper.InsertEnvelopePoint(env, cursor, value, 0, 0, true) + end + elseif line[2] == 'Volume' then + if tween then + local ret, vol, pan = reaper.GetTrackUIVolPan(track) + table.insert(params_to_tween, { track, 'Volume', null, null, vol, value }) + else + reaper.SetMediaTrackInfo_Value(track, "D_VOL", value) + end + elseif line[2] == 'Pan' then + if tween then + local ret, vol, pan = reaper.GetTrackUIVolPan(track) + table.insert(params_to_tween, { track, 'Pan', null, null, pan, value }) + else + reaper.SetMediaTrackInfo_Value(track, "D_PAN", value) + end + elseif line[2] == 'Mute' then + reaper.SetMediaTrackInfo_Value(track, "B_MUTE", value) + end + end + elseif #line == 4 then -- fx params + fxid = fxs[tostring(line[2])] + param = tonumber(line[3]) + value = tonumber(line[4]) + if fxid ~= nil then + if write then + env = reaper.GetFXEnvelope(track, fxid, param, true) + reaper.InsertEnvelopePoint(env, cursor, value, 0, 0, true) + else + if tween then + local val = reaper.TrackFX_GetParam(track, fxid, param) + table.insert(params_to_tween, { track, param, fxid, null, val, value }) + else + reaper.TrackFX_SetParam(track, fxid, param, value) + end + end + else + log('fx not found '..tostring(line)) + logtable(line) + end + elseif #line == 7 then -- sends + local key = line[4] + local count = line[3] + send = sends[key..count] + if send and globals.ui_checkbox_sends then + cnt = tonumber(line[3]) + key = line[4] + vol = line[5] + pan = line[6] + mut = line[7] + if vol ~= 'unchanged' then + if write then insertSendEnvelopePoint(track, key, cnt, tonumber(vol), 'VOLENV') + else + if tween then + local ret, svol, span = reaper.GetTrackSendUIVolPan(track, send) + table.insert(params_to_tween, { track, 'Volume', null, send, tonumber(svol), tonumber(vol) }) + else + reaper.SetTrackSendUIVol(track, send, tonumber(vol), -1) + end + end + end + if pan ~= 'unchanged' then + if write then + pan = -pan -- FIX flip pan before writting to playlist + insertSendEnvelopePoint(track, key, cnt, tonumber(pan), 'PANENV') + else + if tween then + local ret, svol, span = reaper.GetTrackSendUIVolPan(track, send) + table.insert(params_to_tween, { track, 'Pan', null, send, tonumber(span), tonumber(pan) }) + else + reaper.SetTrackSendUIPan(track, send, tonumber(pan), -1) + end + end + end + -- TODO send mute + -- if mut ~= 'unchanged' then + -- if write then insertSendEnvelopePoint(track, key, cnt, tonumber(vol), 'MUTEENV') + -- else reaper.SetTrackSendUIMute(track, send, tonumber(mut), -1) end + -- else reaper.SetTrackSendInfo_Value(track, 0, cnt, 'B_MUTE', tonumber(mut)) end + -- end + end + end + end + end + end +end + +function savesnap(slot) + slot = slot or 1 + snap = makesnap() + reaper.SetProjExtState(0, 'snapshooter', 'snap'..slot, stringify(snap)) + reaper.SetProjExtState(0, 'snapshooter', 'snapdate'..slot, os.date()) +end + +function applysnap(slot, write) + reaper.Undo_BeginBlock() + reaper.PreventUIRefresh(1) + seltracks = {} + for i = 0, reaper.CountSelectedTracks(0) do + table.insert(seltracks, reaper.GetSelectedTrack(0, i)) + end + -- local globaloverride = reaper.GetGlobalAutomationOverride() + -- reaper.SetGlobalAutomationOverride(6) + exists, snap1 = reaper.GetProjExtState(0, 'snapshooter', 'snap'..slot) + if exists then + snap2 = makesnap() + snap2 = parse(stringify(snap2)) -- normalize + diff = difference(parse(snap1), snap2) + use_tween = globals.tween ~= 'none' + if use_tween then + params_to_tween = {} + end + applydiff(diff, write, use_tween) + reaper.defer(reaper.UpdateArrange) + if use_tween then + bpm, bpi = reaper.GetProjectTimeSignature() + duration = 60 / bpm * bpi + if (globals.tween == '1/4bar') then duration = duration / 4 end + if (globals.tween == '1/2bar') then duration = duration / 2 end + if (globals.tween == '2bar') then duration = duration * 2 end + if (globals.tween == '4bar') then duration = duration * 4 end + ease_fn = tween_fns.linear + if (globals.ease == 'easein') then ease_fn = tween_fns.ease_in end + if (globals.ease == 'easeout') then ease_fn = tween_fns.ease_out end + if (globals.ease == 'easeinout') then ease_fn = tween_fns.ease_in_out end + tween_params(reaper.time_precise(), duration, ease_fn) + end + else + log('could not find snap'..slot) + end + reaper.Main_OnCommand(40297, 0) -- Track: Unselect (clear selection of) all tracks + for i, track in ipairs(seltracks) do + reaper.SetTrackSelected(track, 1) -- restore selected tracks + end + -- reaper.SetGlobalAutomationOverride(globaloverride) + reaper.PreventUIRefresh(-1) + reaper.Undo_EndBlock('apply snapshot', 0) +end + +function clearsnap(slot) + reaper.SetProjExtState(0, 'snapshooter', 'snap'..slot, '') + reaper.SetProjExtState(0, 'snapshooter', 'snapdate'..slot, '') +end + +-------------------------------------------------------------------------------- +-- Tween +-------------------------------------------------------------------------------- +tween_fns = {} +function tween_fns.linear (t, b, _c, d) -- t: current time, b: beginning value, _c: final value, d: total duration + local c = _c - b; + return c * t / d + b; +end +function tween_fns.ease_in (t, b, _c, d) + local c = _c - b; + t = t / d + return c * t * t + b; +end +function tween_fns.ease_out (t, b, _c, d) + local var c = _c - b; + t = t / d + return -c * t * (t - 2) + b; +end +function tween_fns.ease_in_out (t, b, _c, d) + local c = _c - b; + t = t / (d / 2) + if ((t) < 1) then + return c / 2 * t * t + b; + else + t = t - 1 + return -c / 2 * (t * (t - 2) - 1) + b; + end +end + +params_to_tween = {} -- {{ track, param, fxid, send, from, to }} +function apply_tween (track, param, fxid, send, value) + if send then + if param == 'Volume' then + reaper.SetTrackSendUIVol(track, send, value, -1) + end + if param == 'Pan' then + reaper.SetTrackSendUIPan(track, send, value, -1) + end + elseif fxid then + reaper.TrackFX_SetParam(track, fxid, param, value) + elseif param == 'Volume' then + reaper.SetMediaTrackInfo_Value(track, "D_VOL", value) + elseif param == 'Pan' then + reaper.SetMediaTrackInfo_Value(track, "D_PAN", value) + end +end + +function tween_params(start, duration, ease_fn) + local delta = reaper.time_precise() - start + if (delta >= duration) then + for i, entry in ipairs(params_to_tween) do + apply_tween(entry[1], entry[2], entry[3], entry[4], entry[6]) + end + return + end + for i, entry in ipairs(params_to_tween) do + local val = ease_fn(delta, entry[5], entry[6], duration) + apply_tween(entry[1], entry[2], entry[3], entry[4], val) + end + reaper.defer(function () tween_params(start, duration, ease_fn) end) +end + +-------------------------------------------------------------------------------- +-- UI +-------------------------------------------------------------------------------- + +ui_snaprows = {} +function ui_refresh_buttons() + for i, row in ipairs(ui_snaprows) do + hassnap = reaper.GetProjExtState(0, 'snapshooter', 'snap'..i) ~= 0 + row.children[1][1]:attr('color', hassnap and 0x99BD13 or 0x999999) -- savebtn + row.children[2][1]:attr('color', hassnap and 0xffffff or 0x777777) -- savetxt + row.children[3][1]:attr('color', hassnap and 0x999999 or 0x555555) -- applybtn + row.children[4][1]:attr('color', hassnap and 0xffffff or 0x777777) -- applytxt + row.children[5][1]:attr('color', hassnap and 0x999999 or 0x555555) -- writebtn + row.children[6][1]:attr('color', hassnap and 0xffffff or 0x777777) -- writetxt + row.children[7][1]:attr('color', hassnap and 0x999999 or 0x555555) -- delbtn + row.children[8][1]:attr('color', hassnap and 0xffffff or 0x777777) -- deltxt + + status, datestr = reaper.GetProjExtState(0, 'snapshooter', 'snapdate'..i) + row.children[2][1]:attr('text', hassnap and ' '..datestr or ' Empty') + end +end + +function ui_start() + -- package.path = reaper.GetResourcePath() .. '/Scripts/rtk/1/?.lua' + -- local rtk = require('rtk') + local sep = package.config:sub(1, 1) + local script_folder = debug.getinfo(1).source:match("@?(.*[\\|/])") + local rtk = dofile(script_folder .. 'tilr_Snapshooter' .. sep .. 'rtk.lua') + local window = rtk.Window{w=470, h=425} + window:open{align='center'} + local box = window:add(rtk.VBox{margin=10}) + box:add(rtk.Heading{'Snapshooter', bmargin=10}) + + for i = 1, 12 do + local row = box:add(rtk.HBox{bmargin=5}) + local button = row:add(rtk.Button{circular=true}) + local text = row:add(rtk.Text{string.format("%02d",i)..' Empty', w=230, textalign='center', lmargin=10}) + button.onclick = function () + savesnap(i) + ui_refresh_buttons() + end + local button = row:add(rtk.Button{circular=true, lmargin=5}) + local text = row:add(rtk.Text{'Apply ', lmargin=5}) + button.onclick = function () + applysnap(i, false) + end + local button = row:add(rtk.Button{circular=true, lmargin=5}) + local text = row:add(rtk.Text{'Write ', lmargin=5}) + button.onclick = function () + applysnap(i, true) + end + local button = row:add(rtk.Button{circular=true, lmargin=5}) + local text = row:add(rtk.Text{'Del ', lmargin=5, lmargin=5}) + button.onclick = function() + clearsnap(i) + ui_refresh_buttons() + end + table.insert(ui_snaprows, row) + end + + -- checkboxes + local row = box:add(rtk.HBox{tmargin=10}) + local ui_checkbox_seltracks = row:add(rtk.CheckBox{'Selected tracks'}) + local ui_checkbox_volume = row:add(rtk.CheckBox{'Vol', lmargin=15}) + local ui_checkbox_pan = row:add(rtk.CheckBox{'Pan',lmargin=15}) + local ui_checkbox_mute = row:add(rtk.CheckBox{'Mute', lmargin=15}) + local ui_checkbox_sends = row:add(rtk.CheckBox{'Sends', lmargin=15}) + + if globals.ui_checkbox_seltracks then + ui_checkbox_seltracks:toggle() + end + if globals.ui_checkbox_volume then + ui_checkbox_volume:toggle() + end + if globals.ui_checkbox_pan then + ui_checkbox_pan:toggle() + end + if globals.ui_checkbox_mute then + ui_checkbox_mute:toggle() + end + if globals.ui_checkbox_sends then + ui_checkbox_sends:toggle() + end + + ui_checkbox_seltracks.onchange = function(self) + reaper.SetProjExtState(0, 'snapshooter', 'ui_checkbox_seltracks', tostring(self.value)) + globals.ui_checkbox_seltracks = self.value + end + ui_checkbox_volume.onchange = function(self) + reaper.SetProjExtState(0, 'snapshooter', 'ui_checkbox_volume', tostring(self.value)) + globals.ui_checkbox_volume = self.value + end + ui_checkbox_pan.onchange = function(self) + reaper.SetProjExtState(0, 'snapshooter', 'ui_checkbox_pan', tostring(self.value)) + globals.ui_checkbox_pan = self.value + end + ui_checkbox_mute.onchange = function(self) + reaper.SetProjExtState(0, 'snapshooter', 'ui_checkbox_mute', tostring(self.value)) + globals.ui_checkbox_mute = self.value + end + ui_checkbox_sends.onchange = function(self) + reaper.SetProjExtState(0, 'snapshooter', 'ui_checkbox_sends', tostring(self.value)) + globals.ui_checkbox_sends = self.value + end + + -- tweening controls + row = box:add(rtk.HBox{tmargin=10}) + row:add(rtk.Text{'Tween', rmargin=5, tmargin=5}) + local tween_menu = row:add(rtk.OptionMenu{ + menu = { + { 'None', id='none' }, + { '1/4 Bar', id='1/4bar' }, + { '1/2 Bar', id='1/2bar' }, + { '1 Bar', id='1bar'}, + { '2 Bar', id='2bar'}, + { '4 Bar', id='4bar'}, + } + }) + tween_menu:select(globals.tween) + tween_menu.onchange = function (self) + reaper.SetProjExtState(0, 'snapshooter', 'ui_tween_menu', self.selected) + globals.tween = self.selected + end + + row:add(rtk.Text{'Ease', lmargin=20, rmargin=5, tmargin=5}) + local ease_menu = row:add(rtk.OptionMenu{ + menu = { + { 'Linear', id='linear' }, + { 'In', id='easein' }, + { 'Out', id='easeout' }, + { 'InOut', id='easeinout' }, + } + }) + ease_menu:select(globals.ease) + ease_menu.onchange = function (self) + reaper.SetProjExtState(0, 'snapshooter', 'ui_ease_menu', self.selected) + globals.ease = self.selected + end + + ui_refresh_buttons() +end + +if not skip_init then -- skip_init set from other scripts + ui_start() +end + +return { + savesnap = savesnap, + applysnap = applysnap +} +-- interactive dev: + -- reaper.showConsoleMsg(¨chars¨) // show console output + -- reaper.NamedCommandLookup('_RSad7acd2e5dbd41ab15aa68ffdb01c4c5fc82c446',0) + -- reaper.Main_OnCommand(55863, 0) diff --git a/Envelopes/tilr_Snapshooter/rtk.lua b/Envelopes/tilr_Snapshooter/rtk.lua new file mode 100644 index 000000000..fe50a10a2 --- /dev/null +++ b/Envelopes/tilr_Snapshooter/rtk.lua @@ -0,0 +1,5686 @@ +-- @noindex + +-- This is generated code. See https://reapertoolkit.dev/ for more info. +-- version: 1.2.0 +-- build: Wed Mar 23 00:22:10 UTC 2022 +__RTK_VERSION='1.2.0' +rtk=(function() +__mod_rtk_core=(function() +__mod_rtk_log=(function() +local log={levels={[50]='CRITICAL',[40]='ERROR',[30]='WARNING',[20]='INFO',[10]='DEBUG',[9]='DEBUG2',},level=40,timer_threshold=20,named_timers=nil,timers={},queue={},lua_time_start=os.time(),reaper_time_start=reaper.time_precise(),}log.CRITICAL=50 +log.ERROR=40 +log.WARNING=30 +log.INFO=20 +log.DEBUG=10 +log.DEBUG2=9 +function log.critical(fmt,...)log._log(log.CRITICAL,nil,fmt,...)end +function log.error(fmt,...)log._log(log.ERROR,nil,fmt,...)end +function log.warning(fmt,...)log._log(log.WARNING,nil,fmt,...)end +function log.info(fmt,...)log._log(log.INFO,nil,fmt,...)end +function log.debug(fmt,...)log._log(log.DEBUG,nil,fmt,...)end +function log.debug2(fmt,...)log._log(log.DEBUG2,nil,fmt,...)end +local function enqueue(msg)local qlen=#log.queue +if qlen==0 then +reaper.defer(log.flush)end +log.queue[qlen+1]=msg +end +local function _get_precise_duration_string(t)if t<0.1 then +return string.format('%.03f', t)elseif t<1 then +return string.format('%.02f', t)elseif t<10 then +return string.format('%.01f', t)else +return string.format('%.0f', t)end +end +function log.exception(fmt,...)log._log(log.ERROR,debug.traceback(),fmt,...)log.flush()end +function log.trace(level)if log.level<=(level or log.DEBUG)then +enqueue(debug.traceback() .. '\n')end +end +function log._log(level,tail,fmt,...)if level0 then +local timer=log.timers[#log.timers] +local total=_get_precise_duration_string((now-timer[1])*1000)local last=_get_precise_duration_string((now-timer[2])*1000)local name=timer[3] and string.format(' [%s]', timer[3]) or ''prefix=prefix .. string.format('(%s / %s ms%s) ', last, total, name)timer[2]=now +end +local msg=prefix .. err .. '\n'if tail then +msg=msg .. tail .. '\n'end +enqueue(msg)end +function log.log(level,fmt,...)return log._log(level,nil,fmt,...)end +function log.logf(level,fmt,func)if level>=log.level then +return log._log(level,nil,fmt,func())end +end +function log.flush()local str=table.concat(log.queue)if #str>0 then +reaper.ShowConsoleMsg(str)end +log.queue={}end +function log.level_name(level)return log.levels[level or log.level] or 'UNKNOWN'end +function log.clear(level)if not level or log.level<=level then +reaper.ShowConsoleMsg("")log.queue={}end +end +function log.time_start(name)if log.level>log.timer_threshold then +return +end +local now=reaper.time_precise()table.insert(log.timers,{now,now,name})if name then +if not log.named_timers then +log.named_timers={}log.named_timers_order={}end +if not log.named_timers[name] then +log.named_timers[name]={0,0}log.named_timers_order[#log.named_timers_order+1]=name +end +end +end +function log.time_end(fmt,...)if fmt then +log._log(log.DEBUG,nil,fmt,...)end +log.time_end_report_if(false)end +function log.time_end_report(fmt,...)if fmt then +log._log(log.DEBUG,nil,fmt,...)end +log.time_end_report_if(true)end +function log.time_end_report_if(show,fmt,...)if log.level>log.timer_threshold then +return +end +if fmt and show then +log._log(log.DEBUG,nil,fmt,...)end +assert(#log.timers > 0, "time_end() with no previous time_start()")local t0,_,name=table.unpack(table.remove(log.timers))if log.named_timers then +if name then +local delta=reaper.time_precise()-t0 +local current=log.named_timers[name] +if not current then +log.named_timers[name]={current+delta,1}else +log.named_timers[name]={current[1]+delta,current[2]+1}end +end +if show and log.level<=log.INFO then +local output=''local maxname=0 +local maxtime=0 +local times={}for i,name in ipairs(log.named_timers_order)do +local duration,_=table.unpack(log.named_timers[name])times[#times+1]=string.format('%.4f ms', duration * 1000)maxtime=math.max(maxtime,#times[#times])maxname=math.max(maxname,#name)end +local fmt=string.format(' %%2d. %%%ds: %%%ds (%%d)\n', maxname, maxtime)for i,name in ipairs(log.named_timers_order)do +local _,count=table.unpack(log.named_timers[name])output=output..string.format(fmt,i,name,times[i],count)end +enqueue(output)end +end +if #log.timers==0 then +log.named_timers=nil +end +end +return log +end)() + +local log=__mod_rtk_log +local rtk={touchscroll=false,smoothscroll=true,touch_activate_delay=0.1,long_press_delay=0.5,double_click_delay=0.5,tooltip_delay=0.5,light_luma_threshold=0.6,debug=false,has_js_reascript_api=(reaper.JS_Window_GetFocus~=nil),has_sws_extension=(reaper.BR_Win32_GetMonitorRectFromRect~=nil),script_path=nil,reaper_hwnd=nil,fps=30,focused_hwnd=nil,focused=nil,theme=nil,_dest_stack={},_image_paths={},_animations={},_animations_len=0,_easing_functions={},_frame_count=0,_frame_time=nil,_modal=nil,_touch_activate_event=nil,_last_traceback=nil,_last_error=nil,_quit=false,_refs=setmetatable({}, {__mode='v'}),_run_soon=nil,_reactive_attr={},}rtk.scale=setmetatable({user=nil,_user=1.0,system=nil,reaper=1.0,framebuffer=nil,value=1.0,_discover=function()local inifile=reaper.get_ini_file()local ini,err=rtk.file.read(inifile)if not err then +rtk.scale.reaper = ini:match('uiscale=([^\n]*)') or 1.0 +end +local ok, dpi=reaper.ThemeLayout_GetLayout("mcp", -3)if not ok then +return +end +dpi=tonumber(dpi)rtk.scale.system=dpi/256.0 +if not rtk.scale.framebuffer then +if rtk.os.mac and dpi==512 then +rtk.scale.framebuffer=2 +else +rtk.scale.framebuffer=1 +end +end +rtk.scale._calc()end,_calc=function()local value=rtk.scale.user*rtk.scale.system*rtk.scale.reaper +rtk.scale.value=math.ceil(value*100)/100.0 +end,},{__index=function(t,key)return key=='user' and t._user or nil +end,__newindex=function(t,key,value)if key=='user' then +if value~=t._user then +t._user=value +rtk.scale._calc()if rtk.window then +rtk.window:queue_reflow()end +end +else +rawset(t,key,value)end +end +})rtk.dnd={dragging=nil,droppable=nil,dropping=nil,arg=nil,buttons=nil,}local _os=reaper.GetOS():lower():sub(1,3)rtk.os={mac = (_os == 'osx' or _os == 'mac'),windows=(_os=='win'),linux = (_os == 'lin' or _os == 'oth'),bits=32,}rtk.mouse={BUTTON_LEFT=1,BUTTON_MIDDLE=64,BUTTON_RIGHT=2,BUTTON_MASK=(1|2|64),x=0,y=0,down=0,state={order={}}}local _load_cursor +if rtk.has_js_reascript_api then +function _load_cursor(cursor)return reaper.JS_Mouse_LoadCursor(cursor)end +else +function _load_cursor(cursor)return cursor +end +end +rtk.mouse.cursors={UNDEFINED=0,POINTER=_load_cursor(32512),BEAM=_load_cursor(32513),LOADING=_load_cursor(32514),CROSSHAIR=_load_cursor(32515),UP_ARROW=_load_cursor(32516),SIZE_NW_SE=_load_cursor(rtk.os.linux and 32643 or 32642),SIZE_SW_NE=_load_cursor(rtk.os.linux and 32642 or 32643),SIZE_EW=_load_cursor(32644),SIZE_NS=_load_cursor(32645),MOVE=_load_cursor(32646),INVALID=_load_cursor(32648),HAND=_load_cursor(32649),POINTER_LOADING=_load_cursor(32650),POINTER_HELP=_load_cursor(32651),REAPER_FADEIN_CURVE=_load_cursor(105),REAPER_FADEOUT_CURVE=_load_cursor(184),REAPER_CROSSFADE=_load_cursor(463),REAPER_DRAGDROP_COPY=_load_cursor(182),REAPER_DRAGDROP_RIGHT=_load_cursor(1011),REAPER_POINTER_ROUTING=_load_cursor(186),REAPER_POINTER_MOVE=_load_cursor(187),REAPER_POINTER_MARQUEE_SELECT=_load_cursor(488),REAPER_POINTER_DELETE=_load_cursor(464),REAPER_POINTER_LEFTRIGHT=_load_cursor(465),REAPER_POINTER_ARMED_ACTION=_load_cursor(434),REAPER_MARKER_HORIZ=_load_cursor(188),REAPER_MARKER_VERT=_load_cursor(189),REAPER_ADD_TAKE_MARKER=_load_cursor(190),REAPER_TREBLE_CLEF=_load_cursor(191),REAPER_BORDER_LEFT=_load_cursor(417),REAPER_BORDER_RIGHT=_load_cursor(418),REAPER_BORDER_TOP=_load_cursor(419),REAPER_BORDER_BOTTOM=_load_cursor(421),REAPER_BORDER_LEFTRIGHT=_load_cursor(450),REAPER_VERTICAL_LEFTRIGHT=_load_cursor(462),REAPER_GRID_RIGHT=_load_cursor(460),REAPER_GRID_LEFT=_load_cursor(461),REAPER_HAND_SCROLL=_load_cursor(429),REAPER_FIST_LEFT=_load_cursor(430),REAPER_FIST_RIGHT=_load_cursor(431),REAPER_FIST_BOTH=_load_cursor(453),REAPER_PENCIL=_load_cursor(185),REAPER_PENCIL_DRAW=_load_cursor(433),REAPER_ERASER=_load_cursor(472),REAPER_BRUSH=_load_cursor(473),REAPER_ARP=_load_cursor(502),REAPER_CHORD=_load_cursor(503),REAPER_TOUCHSEL=_load_cursor(515),REAPER_SWEEP=_load_cursor(517),REAPER_FADEIN_CURVE_ALT=_load_cursor(525),REAPER_FADEOUT_CURVE_ALT=_load_cursor(526),REAPER_XFADE_WIDTH=_load_cursor(528),REAPER_XFADE_CURVE=_load_cursor(529),REAPER_EXTMIX_SECTION_RESIZE=_load_cursor(530),REAPER_EXTMIX_MULTI_RESIZE=_load_cursor(531),REAPER_EXTMIX_MULTISECTION_RESIZE=_load_cursor(532),REAPER_EXTMIX_RESIZE=_load_cursor(533),REAPER_EXTMIX_ALLSECTION_RESIZE=_load_cursor(534),REAPER_EXTMIX_ALL_RESIZE=_load_cursor(535),REAPER_ZOOM=_load_cursor(1009),REAPER_INSERT_ROW=_load_cursor(1010),REAPER_RAZOR=_load_cursor(599),REAPER_RAZOR_MOVE=_load_cursor(600),REAPER_RAZOR_ADD=_load_cursor(601),REAPER_RAZOR_ENVELOPE_VERTICAL=_load_cursor(202),REAPER_RAZOR_ENVELOPE_RIGHT_TILT=_load_cursor(203),REAPER_RAZOR_ENVELOPE_LEFT_TILT=_load_cursor(204),}local FONT_FLAG_BOLD=string.byte('b')local FONT_FLAG_ITALICS=string.byte('i') << 8 +local FONT_FLAG_UNDERLINE=string.byte('u') << 16 +rtk.font={BOLD=FONT_FLAG_BOLD,ITALICS=FONT_FLAG_ITALICS,UNDERLINE=FONT_FLAG_UNDERLINE,multiplier=1.0 +}rtk.keycodes={UP=30064,DOWN=1685026670,LEFT=1818584692,RIGHT=1919379572,RETURN=13,ENTER=13,SPACE=32,BACKSPACE=8,ESCAPE=27,TAB=9,HOME=1752132965,END=6647396,INSERT=6909555,DELETE=6579564,F1=26161,F2=26162,F3=26163,F4=26164,F5=26165,F6=26166,F7=26167,F8=26168,F9=26169,F10=6697264,F11=6697265,F12=6697266,}rtk.themes={dark={name='dark',dark=true,light=false,bg='#252525',default_font={'Calibri', 18},tooltip_font={'Segoe UI (TrueType)', 16},tooltip_bg='#ffffff',tooltip_text='#000000',accent='#47abff',accent_subtle='#306088',text='#ffffff',text_faded='#bbbbbb',button='#555555',heading=nil,button_label='#ffffff',button_font=nil,button_gradient_mul=1,button_tag_alpha=0.32,button_normal_gradient=-0.37,button_normal_border_mul=0.7,button_hover_gradient=0.17,button_hover_brightness=0.9,button_hover_mul=1,button_hover_border_mul=1.1,button_clicked_gradient=0.47,button_clicked_brightness=0.9,button_clicked_mul=0.85,button_clicked_border_mul=1,text_font=nil,heading_font={'Calibri', 26},entry_font=nil,entry_bg='#5f5f5f7f',entry_placeholder='#ffffff7f',entry_border_hover='#3a508e',entry_border_focused='#4960b8',entry_selection_bg='#0066bb',popup_bg=nil,popup_bg_brightness=1.5,popup_shadow='#11111166',popup_border='#385074',slider_font=nil,slider='#2196f3',slider_track='#5a5a5a',slider_tick_label=nil,},light={name='light',light=true,dark=false,accent='#47abff',accent_subtle='#a1d3fc',bg='#dddddd',default_font={'Calibri', 18},tooltip_font={'Segoe UI (TrueType)', 16},tooltip_bg='#ffffff',tooltip_text='#000000',button='#dedede',button_label='#000000',button_gradient_mul=1,button_tag_alpha=0.15,button_normal_gradient=-0.28,button_normal_border_mul=0.85,button_hover_gradient=0.12,button_hover_brightness=1,button_hover_mul=1,button_hover_border_mul=0.9,button_clicked_gradient=0.3,button_clicked_brightness=1.0,button_clicked_mul=0.9,button_clicked_border_mul=0.7,text='#000000',text_faded='#555555',heading_font={'Calibri', 26},entry_border_hover='#3a508e',entry_border_focused='#4960b8',entry_bg='#00000020',entry_placeholder='#0000007f',entry_selection_bg='#9fcef4',popup_bg=nil,popup_bg_brightness=1.5,popup_shadow='#11111122',popup_border='#385074',slider='#2196f3',slider_track='#5a5a5a',}}local function _postprocess_theme()local iconstyle=rtk.color.get_icon_style(rtk.theme.bg)rtk.theme.iconstyle=iconstyle +for k,v in pairs(rtk.theme)do +if type(v) == 'string' and v:byte(1) == 35 then +local x={rtk.color.rgba(v)}rtk.theme[k]={rtk.color.rgba(v)}end +end +end +function rtk.add_image_search_path(path,iconstyle)if not path:match('^%a:') and not path:match('^[\\/]') then +path=rtk.script_path..path +end +if iconstyle then +assert(iconstyle == 'dark' or iconstyle == 'light', 'iconstyle must be either light or dark')local paths=rtk._image_paths[iconstyle] or {}paths[#paths+1]=path +rtk._image_paths[iconstyle]=paths +else +rtk._image_paths[#rtk._image_paths+1]=path +end +end +function rtk.set_theme(name,overrides)name=name or rtk.theme.name +assert(rtk.themes[name], 'rtk: theme "' .. name .. '" does not exist in rtk.themes')rtk.theme={}table.merge(rtk.theme,rtk.themes[name])if overrides then +table.merge(rtk.theme,overrides)end +_postprocess_theme()end +function rtk.set_theme_by_bgcolor(color,overrides)local name=rtk.color.luma(color) > rtk.light_luma_threshold and 'light' or 'dark'overrides=overrides or {}overrides.bg=color +rtk.set_theme(name,overrides)end +function rtk.set_theme_overrides(overrides)for _, name in ipairs({'dark', 'light'}) do +if overrides[name] then +rtk.themes[name]=table.merge(rtk.themes[name],overrides[name])if rtk.theme[name] then +rtk.theme=table.merge(rtk.theme,overrides[name])end +overrides[name]=nil +end +end +rtk.themes.dark=table.merge(rtk.themes.dark,overrides)rtk.themes.light=table.merge(rtk.themes.light,overrides)rtk.theme=table.merge(rtk.theme,overrides)_postprocess_theme()end +function rtk.new_theme(name,base,overrides)assert(not base or rtk.themes[base], string.format('base theme %s not found', base))assert(not rtk.themes[name], string.format('theme %s already exists', name))local theme=base and table.shallow_copy(rtk.themes[base])or {}rtk.themes[name]=table.merge(theme,overrides or {})end +function rtk.add_modal(...)if rtk._modal==nil then +rtk._modal={}end +local widgets={...}for _,widget in ipairs(widgets)do +rtk._modal[widget.id]=widget +end +end +function rtk.is_modal(widget)if widget==nil then +return rtk._modal~=nil +elseif rtk._modal then +local w=widget +while w do +if rtk._modal[w.id]~=nil then +return true +end +w=w.parent +end +end +return false +end +function rtk.reset_modal()rtk._modal=nil +end +function rtk.pushdest(dest)rtk._dest_stack[#rtk._dest_stack+1]=gfx.dest +gfx.dest=dest +end +function rtk.popdest(expect)gfx.dest=table.remove(rtk._dest_stack,#rtk._dest_stack)end +local function _handle_error(err)rtk._last_error=err +rtk._last_traceback=debug.traceback()end +function rtk.onerror(err,traceback)log.error("fatal: %s\n%s", err, traceback)log.flush()error(err)end +function rtk.call(func,...)if rtk._quit then +return +end +local ok,result=xpcall(func,_handle_error,...)if not ok then +rtk.onerror(rtk._last_error,rtk._last_traceback)return +end +return result +end +function rtk.defer(func,...)if rtk._quit then +return +end +local args=table.pack(...)reaper.defer(function()rtk.call(func,table.unpack(args,1,args.n))end)end +function rtk.callsoon(func,...)if not rtk.window or not rtk.window.running then +return rtk.defer(func,...)end +local funcs=rtk._soon_funcs +if not funcs then +funcs={}rtk._soon_funcs=funcs +end +funcs[#funcs+1]={func,table.pack(...)}end +function rtk._run_soon()local funcs=rtk._soon_funcs +rtk._soon_funcs=nil +for i=1,#funcs do +local func,args=table.unpack(funcs[i])func(table.unpack(args,1,args.n))end +end +function rtk.callafter(duration,func,...)local args=table.pack(...)local start=reaper.time_precise()local function sched()if reaper.time_precise()-start>=duration then +rtk.call(func,table.unpack(args,1,args.n))elseif not rtk._quit then +reaper.defer(sched)end +end +sched()end +function rtk.quit()if rtk.window and rtk.window.running then +rtk.window:close()end +rtk._quit=true +end +rtk.version={_DEFAULT_API=1,string=nil,api=nil,major=nil,minor=nil,patch=nil,}function rtk.version.parse()local ver=__RTK_VERSION or string.format('%s.99.99', rtk.version._DEFAULT_API)local parts=ver:split('.')rtk.version.major=tonumber(parts[1])rtk.version.minor=tonumber(parts[2])rtk.version.patch=tonumber(parts[3])rtk.version.api=rtk.version.major +rtk.version.string=ver +end +function rtk.version.check(major,minor,patch)local v=rtk.version +return v.major>major or +(v.major==major and(not minor or v.minor>minor))or +(v.major==major and v.minor==minor and(not patch or v.patch>=patch))end +return rtk +end)() + +local rtk=__mod_rtk_core +__mod_rtk_type=(function() +local rtk=__mod_rtk_core +__mod_rtk_middleclass=(function() +local middleclass={_VERSION='middleclass v4.1.1',}local function _createIndexWrapper(aClass,f)if f==nil then +return aClass.__instanceDict +else +return function(self,name)local value=aClass.__instanceDict[name] +if value~=nil then +return value +elseif type(f)=="function" then +return(f(self,name))else +return f[name] +end +end +end +end +local function _propagateInstanceMethod(aClass,name,f)f=name=="__index" and _createIndexWrapper(aClass, f) or f +aClass.__instanceDict[name]=f +for subclass in pairs(aClass.subclasses)do +if rawget(subclass.__declaredMethods,name)==nil then +_propagateInstanceMethod(subclass,name,f)end +end +end +local function _declareInstanceMethod(aClass,name,f)aClass.__declaredMethods[name]=f +if f==nil and aClass.super then +f=aClass.super.__instanceDict[name] +end +_propagateInstanceMethod(aClass,name,f)end +local function _tostring(self) return "class " .. self.name end +local function _call(self,...)return self:new(...)end +local function _createClass(name,super)local dict={}dict.__index=dict +local aClass={ name=name,super=super,static={},__instanceDict=dict,__declaredMethods={},subclasses=setmetatable({}, {__mode='k'}) }if super then +setmetatable(aClass.static,{__index=function(_,k)local result=rawget(dict,k)if result==nil then +return super.static[k] +end +return result +end +})else +setmetatable(aClass.static,{ __index=function(_,k)return rawget(dict,k)end })end +setmetatable(aClass,{ __index=aClass.static,__tostring=_tostring,__call=_call,__newindex=_declareInstanceMethod })return aClass +end +local function _includeMixin(aClass,mixin)assert(type(mixin)=='table', "mixin must be a table")for name,method in pairs(mixin)do +if name ~= "included" and name ~= "static" then aClass[name] = method end +end +for name,method in pairs(mixin.static or {})do +aClass.static[name]=method +end +if type(mixin.included)=="function" then mixin:included(aClass) end +return aClass +end +local DefaultMixin={__tostring=function(self) return "instance of " .. tostring(self.class) end,__gc=function(self)if type(self) == 'table' and type(self.class) == 'table' and type(self.class.finalize) == 'function' then +self:finalize()end +end,initialize=function(self,...)end,isInstanceOf=function(self,aClass)return type(aClass)=='table'and type(self)=='table'and(self.class==aClass +or type(self.class)=='table'and type(self.class.isSubclassOf)=='function'and self.class:isSubclassOf(aClass))end,static={allocate=function(self)assert(type(self)=='table', "Make sure that you are using 'Class:allocate' instead of 'Class.allocate'")local instance=setmetatable({ class=self },self.__instanceDict)if instance.__allocate then +instance:__allocate()end +return instance +end,new=function(self,...)assert(type(self)=='table', "Make sure that you are using 'Class:new' instead of 'Class.new'")local instance=self:allocate()instance:initialize(...)return instance +end,subclass=function(self,name)assert(type(self)=='table', "Make sure that you are using 'Class:subclass' instead of 'Class.subclass'")assert(type(name)=="string", "You must provide a name(string) for your class")local subclass=_createClass(name,self)for methodName,f in pairs(self.__instanceDict)do +_propagateInstanceMethod(subclass,methodName,f)end +subclass.initialize=function(instance,...)return self.initialize(instance,...)end +self.subclasses[subclass]=true +self:subclassed(subclass)return subclass +end,subclassed=function(self,other)end,isSubclassOf=function(self,other)return type(other)=='table' and +type(self.super)=='table' and +(self.super==other or self.super:isSubclassOf(other))end,include=function(self,...)assert(type(self)=='table', "Make sure you that you are using 'Class:include' instead of 'Class.include'")for _,mixin in ipairs({...})do _includeMixin(self,mixin)end +return self +end +}}function middleclass.class(name,super)assert(type(name)=='string', "A name (string) is needed for the new class")return super and super:subclass(name)or _includeMixin(_createClass(name),DefaultMixin)end +setmetatable(middleclass,{ __call=function(_,...)return middleclass.class(...)end })return middleclass +end)() + +local class=__mod_rtk_middleclass +rtk.Attribute={FUNCTION={},NIL={},DEFAULT={},default=nil,type=nil,calculate=nil,priority=nil,reflow=nil,redraw=nil,replaces=nil,animate=nil,get=nil,set=nil,}setmetatable(rtk.Attribute,{__call=function(self,attrs)attrs._is_rtk_attr=true +return attrs +end +})local falsemap={[false]=true,[0]=true,['0']=true,['false']=true,['False']=true,['FALSE']=true +}local typemaps={number=function(v)local n=tonumber(v)if n then +return n +elseif v == 'true' or v == true then +return 1 +elseif v == 'false' or v == false then +return 0 +end +end,string=tostring,boolean=function(v)if falsemap[v] then +return false +elseif v then +return true +end +end,}function rtk.Reference(attr)return {_is_rtk_reference=true,attr=attr +}end +local function register(cls,attrs)local attributes=cls.static.attributes +if attributes and attributes.__class==cls.name then +elseif cls.super then +attributes={}for k,v in pairs(cls.super.static.attributes)do +if k ~= '__class' and k ~= 'get' then +attributes[k]=table.shallow_copy(v)end +end +else +attributes={defaults={}}end +local refs={}for attr,attrtable in pairs(attrs)do +assert(attr ~= 'id' and attr ~= 'get' and attr ~= 'defaults',"attempted to assign a reserved attribute")if type(attrtable)=='table' and attrtable._is_rtk_reference then +local srcattr=attrtable.attr +attrtable={}refs[#refs+1]={attrtable,nil,srcattr,attr}else +if type(attrtable) ~='table' or not attrtable._is_rtk_attr then +attrtable={default=attrtable}end +if attributes[attr] then +attrtable=table.merge(attributes[attr],attrtable)end +for field,v in pairs(attrtable)do +if type(v)=='table' and v._is_rtk_reference then +refs[#refs+1]={attrtable,field,v.attr,attr}end +end +local deftype=type(attrtable.default)if deftype=='function' then +attrtable.default_func=attrtable.default +attrtable.default=rtk.Attribute.FUNCTION +end +if (not attrtable.type and not attrtable.calculate) or type(attrtable.type)=='string' then +attrtable.type=typemaps[attrtable.type or deftype] +end +end +attributes[attr]=attrtable +attributes.defaults[attr]=attrtable.default +end +for _,ref in ipairs(refs)do +local attrtable,field,srcattr,dstattr=table.unpack(ref)local src=attributes[srcattr] +if not attributes.defaults[dstattr] and not field then +attributes.defaults[dstattr]=attributes.defaults[srcattr] +end +if field then +attrtable[field]=src[field] +else +for k,v in pairs(src)do +attrtable[k]=v +end +end +end +attributes.__class=cls.name +attributes.get=function(attr)return attributes[attr] or rtk.Attribute.NIL +end +cls.static.attributes=attributes +end +function rtk.class(name,super,attributes)local cls=class(name,super)cls.static.register=function(attrs)register(cls,attrs)end +if attributes then +register(cls,attributes)end +return cls +end +function rtk.isa(v,cls)if type(v)=='table' and v.isInstanceOf then +return v:isInstanceOf(cls)end +return false +end +end)() + +__mod_rtk_utils=(function() +local rtk=__mod_rtk_core +rtk.file={}rtk.clipboard={}rtk.gfx={}UNDO_STATE_ALL=-1 +UNDO_STATE_TRACKCFG=1 +UNDO_STATE_FX=2 +UNDO_STATE_ITEMS=4 +UNDO_STATE_MISCCFG=8 +UNDO_STATE_FREEZE=16 +UNDO_STATE_TRACKENV=32 +UNDO_STATE_FXENV=64 +UNDO_STATE_POOLEDENVS=128 +UNDO_STATE_FX_ARA=256 +function rtk.check_reaper_version(major,minor,exact)local curmaj=rtk._reaper_version_major +local curmin=rtk._reaper_version_minor +minor=minor<100 and minor or minor/10 +if exact then +return curmaj==major and curmin==minor +else +return(curmaj>major)or(curmaj==major and curmin>=minor)end +end +function rtk.clamp(value,min,max)if min and max then +return math.max(min,math.min(max,value))elseif min then +return math.max(min,value)elseif max then +return math.min(max,value)else +return value +end +end +function rtk.clamprel(value,min,max)min=min and min<1.0 and min*value or min +max=max and max<1.0 and max*value or max +if min and max then +return math.max(min,math.min(max,value))elseif min then +return math.max(min,value)elseif max then +return math.min(max,value)else +return value +end +end +function rtk.point_in_box(x,y,bx,by,bw,bh)return x>=bx and y>=by and x<=bx+bw and y<=by+bh +end +function rtk.point_in_circle(x,y,cirx,ciry,radius)local dx=x-cirx +local dy=y-ciry +return dx*dx+dy*dy<=radius*radius +end +function rtk.open_url(url)if rtk.os.windows then +reaper.ExecProcess(string.format('cmd.exe /C start /B "" "%s"', url), -2)elseif rtk.os.mac then +os.execute(string.format('open "%s"', url))elseif rtk.os.linux then +reaper.ExecProcess(string.format('xdg-open "%s"', url), -2)else +reaper.ShowMessageBox("Sorry, I don't know how to open URLs on this operating system.","Unsupported operating system", 0 +)end +end +function rtk.uuid4()return reaper.genGuid():sub(2,-2):lower()end +function rtk.file.read(fname)local f,err=io.open(fname)if f then +local contents=f:read("*all")f:close()return contents,nil +else +return nil,err +end +end +function rtk.file.write(fname,contents)local f, err=io.open(fname, "w")if f then +f:write(contents)f:close()else +return err +end +end +function rtk.file.size(fname)local f,err=io.open(fname)if f then +local size=f:seek("end")f:close()return size,nil +else +return nil,err +end +end +function rtk.file.exists(fname)return reaper.file_exists(fname)end +function rtk.clipboard.get()if not reaper.CF_GetClipboardBig then +return +end +local fast=reaper.SNM_CreateFastString("")local data=reaper.CF_GetClipboardBig(fast)reaper.SNM_DeleteFastString(fast)return data +end +function rtk.clipboard.set(data)if not reaper.CF_SetClipboard then +return false +end +reaper.CF_SetClipboard(data)return true +end +function rtk.gfx.roundrect(x,y,w,h,r,thickness,aa)thickness=thickness or 1 +aa=aa or 1 +w=w-1 +h=h-1 +if thickness==1 then +gfx.roundrect(x,y,w,h,r,aa)elseif thickness>1 then +for i=0,thickness-1 do +gfx.roundrect(x+i,y+i,w-i*2,h-i*2,r,aa)end +elseif h>=2*r then +gfx.circle(x+r,y+r,r,1,aa)gfx.circle(x+w-r,y+r,r,1,aa)gfx.circle(x+r,y+h-r,r,1,aa)gfx.circle(x+w-r,y+h-r,r,1,aa)gfx.rect(x,y+r,r,h-r*2)gfx.rect(x+w-r,y+r,r+1,h-r*2)gfx.rect(x+r,y,w-r*2,h+1)else +r=h/2-1 +gfx.circle(x+r,y+r,r,1,aa)gfx.circle(x+w-r,y+r,r,1,aa)gfx.rect(x+r,y,w-r*2,h)end +end +rtk.IndexManager=rtk.class('rtk.IndexManager')function rtk.IndexManager:initialize(first,last)self.first=first +self.last=last +self._last=last-first +self._bitmaps={}self._tail_idx=nil +self._last_idx=nil +end +function rtk.IndexManager:_set(idx,value)local elem=math.floor(idx/32)+1 +local count=#self._bitmaps +if elem>count then +for n=1,elem-count do +self._bitmaps[#self._bitmaps+1]=0 +end +end +local bit=idx%32 +if value~=0 then +self._bitmaps[elem]=self._bitmaps[elem]|(1<#self._bitmaps then +return false +end +local bit=idx%32 +return self._bitmaps[elem]&(1<=self._last%32 then +return nil +end +idx=(elem-1)*32+bit +end +self._last_idx=idx +self._tail_idx=self._tail_idx and math.max(self._tail_idx,idx)or idx +self:_set(idx,1)return idx+self.first +end +function rtk.IndexManager:next(gc)local idx=self:_next()if not idx and gc then +collectgarbage('collect')idx=self:_next()end +return idx +end +function rtk.IndexManager:release(idx)self:_set(idx-self.first,0)end +math.inf=1/0 +function math.round(n)return n and(n%1>=0.5 and math.ceil(n)or math.floor(n))end +function string.startswith(s,prefix,insensitive)if insensitive==true then +return s:lower():sub(1,string.len(prefix))==prefix:lower()else +return s:sub(1,string.len(prefix))==prefix +end +end +function string.split(s,delim,filter)local parts={}for word in s:gmatch('[^' .. (delim or '%s') .. ']' .. (filter and '+' or '*')) do +parts[#parts+1]=word +end +return parts +end +function string.strip(s)return s:match('^%s*(.-)%s*$')end +function string.hash(s)local hash=5381 +for i=1,#s do +hash=((hash<<5)+hash)+s:byte(i)end +return hash&0x7fffffffffffffff +end +local function val_to_str(v,seen)if "string" == type(v) then +v=string.gsub(v, "\n", "\\n")if string.match(string.gsub(v,"[^'\"]",""), '^"+$') then +return "'" .. v .. "'"end +return '"' .. string.gsub(v, '"', '\\"') .. '"'else +if type(v)=='table' and not v.__tostring then +return seen[tostring(v)] and '' or table.tostring(v, seen)else +return tostring(v)end +return "table" == type(v) and table.tostring(v, seen) or tostring(v)end +end +local function key_to_str(k,seen)if "string" == type(k) and string.match(k, "^[_%a][_%a%d]*$") then +return k +else +return "[" .. val_to_str(k, seen) .. "]"end +end +local function _table_tostring(tbl,seen)local result,done={},{}seen=seen or {}local id=tostring(tbl)seen[id]=1 +for k,v in ipairs(tbl)do +table.insert(result,val_to_str(v,seen))done[k]=true +end +for k,v in pairs(tbl)do +if not done[k] then +table.insert(result, key_to_str(k, seen) .. "=" .. val_to_str(v, seen))end +end +seen[id]=nil +return "{" .. table.concat( result, "," ) .. "}"end +function table.tostring(tbl)return _table_tostring(tbl)end +function table.fromstring(str)return load('return ' .. str)()end +function table.merge(dst,src)for k,v in pairs(src)do +dst[k]=v +end +return dst +end +function table.shallow_copy(t,merge)local copy={}for k,v in pairs(t)do +copy[k]=v +end +if merge then +table.merge(copy,merge)end +return copy +end +function table.keys(t)local keys={}for k,_ in pairs(t)do +keys[#keys+1]=k +end +return keys +end +function table.values(t)local values={}for _,v in pairs(t)do +values[#values+1]=v +end +return values +end +end)() + +__mod_rtk_future=(function() +local rtk=__mod_rtk_core +rtk.Future=rtk.class('rtk.Future')rtk.Future.static.PENDING=false +rtk.Future.static.DONE=true +rtk.Future.static.CANCELLED=0 +function rtk.Future:initialize()self.state=rtk.Future.PENDING +self.result=nil +self.cancellable=false +end +function rtk.Future:after(func)if not self._after then +self._after={func}else +self._after[#self._after+1]=func +end +self:_check_defer_resolved_callbacks(rtk.Future.DONE)return self +end +function rtk.Future:done(func)if not self._done then +self._done={func}else +self._done[#self._done+1]=func +end +self:_check_defer_resolved_callbacks(rtk.Future.DONE)return self +end +function rtk.Future:cancelled(func)self.cancellable=true +if self.state==rtk.Future.CANCELLED then +func(self.result)elseif not self._cancelled then +self._cancelled={func}else +self._cancelled[#self._cancelled+1]=func +end +return self +end +function rtk.Future:cancel(v)assert(self._cancelled, 'Future is not cancelleable')assert(self.state==rtk.Future.PENDING, 'Future has already been resolved or cancelled')self.state=rtk.Future.CANCELLED +self.result=v +for i=1,#self._cancelled do +self._cancelled[i](v)end +self._cancelled=nil +return self +end +function rtk.Future:_resolve(value)self.result=value +self:_invoke_resolved_callbacks(value)end +function rtk.Future:_check_defer_resolved_callbacks(state,value)if self.state==state and not self._deferred then +self._deferred=true +rtk.defer(rtk.Future._invoke_resolved_callbacks,self,value or self.value)end +end +function rtk.Future:_invoke_resolved_callbacks(value)self._deferred=false +self.result=value +local nextval=value +if self._after then +while #self._after>0 do +local func=table.remove(self._after,1)nextval=func(nextval)or nextval +if rtk.isa(nextval,rtk.Future)then +nextval:done(function(v)self:_resolve(v)end)self:cancelled(function(v)nextval:cancel(v)end)return +end +end +end +self.state=rtk.Future.DONE +if self._done and(not self._after or #self._after==0)then +for i=1,#self._done do +self._done[i](nextval)end +end +self._done=nil +self._after=nil +return self +end +function rtk.Future:resolve(value)assert(self.state==rtk.Future.PENDING, 'Future has already been resolved or cancelled')if not self._after and not self._done and not self._deferred then +self._deferred=true +rtk.defer(self._resolve,self,value,true)else +self:_resolve(value)end +return self +end +end)() + +__mod_rtk_animate=(function() +local rtk=__mod_rtk_core +local log=__mod_rtk_log +local c1=1.70158 +local c2=c1*1.525 +local c3=c1+1 +local c4=(2*math.pi)/3 +local c5=(2*math.pi)/4.5 +local n1=7.5625 +local d1=2.75 +rtk.easing={['linear'] = function(x)return x +end,['in-sine'] = function(x)return 1-math.cos((x*math.pi)/2)end,['out-sine'] = function(x)return math.sin((x*math.pi)/2)end,['in-out-sine'] = function(x)return-(math.cos(math.pi*x)-1)/2 +end,['in-quad'] = function(x)return x*x +end,['out-quad'] = function(x)return 1-(1-x)*(1-x)end,['in-out-quad'] = function(x)return(x<0.5)and(2*x*x)or(1-(-2*x+2)^2/2)end,['in-cubic'] = function(x)return x*x*x +end,['out-cubic'] = function(x)return 1-(1-x)^4 +end,['in-out-cubic'] = function(x)return(x<0.5)and(4*x*x*x)or(1-(-2*x+2)^3/2)end,['in-quart'] = function(x)return x*x*x*x +end,['out-quart'] = function(x)return 1-(1-x)^4 +end,['in-out-quart'] = function(x)return(x<0.5)and(8*x*x*x*x)or(1-(-2*x+2)^4/2)end,['in-quint'] = function(x)return x*x*x*x*x +end,['out-quint'] = function(x)return 1-(1-x)^5 +end,['in-out-quint'] = function(x)return(x<0.5)and(16*x*x*x*x*x)or(1-(-2*x+2)^5/2)end,['in-expo'] = function(x)return(x==0)and 0 or 2^(10*x-10)end,['out-expo'] = function(x)return(x==1)and 1 or(1-2^(-10*x))end,['in-out-expo'] = function(x)return(x==0)and 0 or +(x==1)and 1 or +(x<0.5)and 2^(20*x-10)/2 or(2-2^(-20*x+10))/2 +end,['in-circ'] = function(x)return 1-math.sqrt(1-x^2)end,['out-circ'] = function(x)return math.sqrt(1-(x-1)^2)end,['in-out-circ'] = function(x)return(x<0.5)and(1-math.sqrt(1-(2*x)^2))/2 or(math.sqrt(1-(-2*x+2)^2)+1)/2 +end,['in-back'] = function(x)return c3*x*x*x-c1*x*x +end,['out-back'] = function(x)return 1+(c3*(x-1)^3)+(c1*(x-1)^2)end,['in-out-back'] = function(x)return(x<0.5)and +((2*x)^2*((c2+1)*2*x-c2))/2 or +((2*x-2)^2*((c2+1)*(x*2-2)+c2)+2)/2 +end,['in-elastic'] = function(x)return(x==0)and 0 or +(x==1)and 1 or +-2^(10*x-10)*math.sin((x*10-10.75)*c4)end,['out-elastic'] = function(x)return(x==0)and 0 or +(x==1)and 1 or +2^(-10*x)*math.sin((x*10-0.75)*c4)+1 +end,['in-out-elastic'] = function(x)return(x==0)and 0 or +(x==1)and 1 or +(x<0.5)and-(2^(20*x-10)*math.sin((20*x-11.125)*c5))/2 or +(2^(-20*x+10)*math.sin((20*x-11.125)*c5))/2+1 +end,['in-bounce'] = function(x)return 1 - rtk.easing['out-bounce'](1 - x)end,['out-bounce'] = function(x)if x<1/d1 then +return n1*x*x +elseif x<(2/d1)then +x=x-1.5/d1 +return n1*x*x+0.75 +elseif x<(2.5/d1)then +x=x-2.25/d1 +return n1*x*x+0.9375 +else +x=x-2.625/d1 +return n1*x*x+0.984375 +end +end,['in-out-bounce'] = function(x)return(x<0.5)and +(1 - rtk.easing['out-bounce'](1 - 2 * x)) / 2 or +(1 + rtk.easing['out-bounce'](2 * x - 1)) / 2 +end,}local function _resolve(x,src,dst)return src+x*(dst-src)end +local _table_stepfuncs={[1]=function(widget,anim)local x=anim.easingfunc(anim.pct)return {_resolve(x,anim.src[1],anim.dst[1])}end,[2]=function(widget,anim)local x=anim.easingfunc(anim.pct)local src,dst=anim.src,anim.dst +local f1=_resolve(x,src[1],dst[1])local f2=_resolve(x,src[2],dst[2])return {f1,f2}end,[3]=function(widget,anim)local x=anim.easingfunc(anim.pct)local src,dst=anim.src,anim.dst +local f1=_resolve(x,src[1],dst[1])local f2=_resolve(x,src[2],dst[2])local f3=_resolve(x,src[3],dst[3])return {f1,f2,f3}end,[4]=function(widget,anim)local x=anim.easingfunc(anim.pct)local src,dst=anim.src,anim.dst +local f1=_resolve(x,src[1],dst[1])local f2=_resolve(x,src[2],dst[2])local f3=_resolve(x,src[3],dst[3])local f4=_resolve(x,src[4],dst[4])return {f1,f2,f3,f4}end,any=function(widget,anim)local x=anim.easingfunc(anim.pct)local src,dst=anim.src,anim.dst +local result={}for i=1,#src do +result[i]=_resolve(x,src[i],dst[i])end +return result +end +}function rtk._do_animations(now)if not rtk._frame_times then +rtk._frame_times={now}else +local times=rtk._frame_times +local c=#times +times[c+1]=now +if c>30 then +table.remove(times,1)end +rtk.fps=c/(times[c]-times[1])end +if rtk._animations_len>0 then +local donefuncs=nil +local done=nil +for key,anim in pairs(rtk._animations)do +local widget=anim.widget +local target=anim.target or anim.widget +local attr=anim.attr +local finished=anim.pct>=1.0 +local elapsed=now-anim._start_time +local newval +if anim.stepfunc then +newval=anim.stepfunc(target,anim)else +newval=anim.resolve(anim.easingfunc(anim.pct))end +anim.frames=anim.frames+1 +if not finished and elapsed>anim.duration*1.5 then +log.warning('animation: %s %s - failed to complete within 1.5x of duration (fps: current=%s expected=%s)',target,attr,rtk.fps,anim.startfps)finished=true +end +if anim.update then +anim.update(finished and anim.doneval or newval,target,attr,anim)end +if widget then +if not finished then +local value=anim.calculate and anim.calculate(widget,attr,newval,widget.calc)or newval +widget.calc[attr]=value +if anim.sync_surface_value then +widget[attr]=value +end +else +widget:attr(attr,anim.doneval)end +local reflow=anim.reflow or(anim.attrmeta and anim.attrmeta.reflow)or rtk.Widget.REFLOW_PARTIAL +if reflow and reflow~=rtk.Widget.REFLOW_NONE then +widget:queue_reflow(reflow)end +if anim.attrmeta and anim.attrmeta.window_sync then +widget._sync_window_attrs_on_update=true +end +end +if finished then +rtk._animations[key]=nil +rtk._animations_len=rtk._animations_len-1 +if not done then +done={}end +done[#done+1]=anim +else +anim.pct=anim.pct+anim.pctstep +end +end +if done then +for _,anim in ipairs(done)do +anim.future:resolve(anim.widget or anim.target)local took=reaper.time_precise()-anim._start_time +local missed=took-anim.duration +log.log(math.abs(missed)>0.05 and log.DEBUG or log.DEBUG2,'animation: done %s: %s -> %s on %s frames=%s current-fps=%s expected-fps=%s took=%.1f (missed by %.3f)',anim.attr,anim.src,anim.dst,anim.target or anim.widget,anim.frames,rtk.fps,anim.startfps,took,missed +)end +end +return true +end +end +local function _is_equal(a,b)local ta=type(a)if ta~=type(b)then +return false +elseif ta=='table' then +if #a~=#b then +return false +end +for i=1,#a do +if a[i]~=b[i] then +return false +end +end +return true +end +return a==b +end +function rtk.queue_animation(kwargs)assert(kwargs and kwargs.key, 'animation table missing key field')local future=rtk.Future()local key=kwargs.key +local anim=rtk._animations[key] +if anim then +if _is_equal(anim.dst,kwargs.dst)then +return anim.future +else +anim.future:cancel()end +end +if _is_equal(kwargs.src,kwargs.dst)then +future:resolve()return future +end +future:cancelled(function()rtk._animations[key]=nil +rtk._animations_len=rtk._animations_len-1 +end)local duration=kwargs.duration or 0.5 +local easingfunc=rtk.easing[kwargs.easing or 'linear'] +assert(type(easingfunc)=='function', string.format('unknown easing function: %s', kwargs.easing))if not kwargs.stepfunc then +local tp=type(kwargs.src or 0)if tp=='table' then +local sz=#kwargs.src +for i=1,sz do +assert(type(kwargs.src[i])=='number', 'animation src value table must not have non-numeric elements')end +kwargs.stepfunc=_table_stepfuncs[sz] +if not kwargs.stepfunc then +kwargs.stepfunc=_table_stepfuncs.any +end +else +assert(tp=='number', string.format('animation src value %s is invalid', kwargs.src))end +end +if not rtk._animations[kwargs.key] then +rtk._animations_len=rtk._animations_len+1 +end +local step=1.0/(rtk.fps*duration)anim=table.shallow_copy(kwargs,{easingfunc=easingfunc,src=kwargs.src or(not kwargs.stepfunc and 0 or nil),dst=kwargs.dst or 0,doneval=kwargs.doneval or kwargs.dst,pct=step,pctstep=step,duration=duration,future=future,frames=0,startfps=rtk.fps,_start_time=reaper.time_precise()})anim.resolve=function(x)return _resolve(x,anim.src,anim.dst)end +rtk._animations[kwargs.key]=anim +log.debug2('animation: scheduled %s', kwargs.key)return future +end +end)() + +__mod_rtk_color=(function() +local rtk=__mod_rtk_core +local log=__mod_rtk_log +rtk.color={}function rtk.color.set(color,amul)local r,g,b,a=rtk.color.rgba(color)if amul then +a=a*amul +end +gfx.set(r,g,b,a)end +function rtk.color.rgba(color)local tp=type(color)if tp=='table' then +local r,g,b,a=table.unpack(color)return r,g,b,a or 1 +elseif tp=='string' then +local hash=color:find('#')if hash==1 then +return rtk.color.hex2rgba(color)else +local a=1 +if hash then +a=(tonumber(color:sub(hash+1),16)or 0)/255 +color=color:sub(1,hash-1)end +local resolved=rtk.color.names[color:lower()] +if not resolved then +log.warning('rtk: color "%s" is invalid, defaulting to black', color)return 0,0,0,a +end +local r,g,b,_=rtk.color.hex2rgba(resolved)return r,g,b,a +end +elseif tp=='number' then +local r,g,b=color&0xff,(color>>8)&0xff,(color>>16)&0xff +return r/255,g/255,b/255,1 +else +error('invalid type ' .. tp .. ' passed to rtk.color.rgba()')end +end +function rtk.color.luma(color,under)local r,g,b,a=rtk.color.rgba(color)local luma=(0.2126*r+0.7152*g+0.0722*b)if a<1.0 and under then +luma=math.abs((luma*a)+rtk.color.luma(under)*(1-a))end +return luma +end +function rtk.color.hsv(color)local r,g,b,a=rtk.color.rgba(color)local h,s,v +local max=math.max(r,g,b)local min=math.min(r,g,b)local delta=max-min +if delta==0 then +h=0 +elseif max==r then +h=60*(((g-b)/delta)%6)elseif max==g then +h=60*(((b-r)/delta)+2)elseif max==b then +h=60*(((r-g)/delta)+4)end +s=(max==0)and 0 or(delta/max)v=max +return h/360.0,s,v,a +end +function rtk.color.hsl(color)local r,g,b,a=rtk.color.rgba(color)local h,s,l +local max=math.max(r,g,b)local min=math.min(r,g,b)l=(max+min)/2 +if max==min then +h=0 +s=0 +else +local delta=max-min +if l>0.5 then +s=delta/(2-max-min)else +s=delta/(max+min)end +if max==r then +h=(g-b)/delta+(g>16)&0xff)end +function rtk.color.get_reaper_theme_bg()if reaper.GetThemeColor then +local r=reaper.GetThemeColor('col_tracklistbg', 0)if r~=-1 then +return rtk.color.int2hex(r)end +end +if reaper.GSC_mainwnd then +local idx=(rtk.os.mac or rtk.os.linux)and 5 or 20 +return rtk.color.int2hex(reaper.GSC_mainwnd(idx))end +end +function rtk.color.get_icon_style(color,under)return rtk.color.luma(color, under) > rtk.light_luma_threshold and 'dark' or 'light'end +function rtk.color.hex2rgba(s)local r=tonumber(s:sub(2,3),16)or 0 +local g=tonumber(s:sub(4,5),16)or 0 +local b=tonumber(s:sub(6,7),16)or 0 +local a=tonumber(s:sub(8,9),16)return r/255,g/255,b/255,a and a/255 or 1.0 +end +function rtk.color.rgba2hex(r,g,b,a)r=math.ceil(r*255)b=math.ceil(b*255)g=math.ceil(g*255)if not a or a==1.0 then +return string.format('#%02x%02x%02x', r, g, b)else +return string.format('#%02x%02x%02x%02x', r, g, b, math.ceil(a * 255))end +end +function rtk.color.int2hex(n,native)if native then +n=rtk.color.convert_native(n)end +local r,g,b=n&0xff,(n>>8)&0xff,(n>>16)&0xff +return string.format('#%02x%02x%02x', r, g, b)end +function rtk.color.hsv2rgb(h,s,v,a)if s==0 then +return v,v,v,a or 1.0 +end +local i=math.floor(h*6)local f=(h*6)-i +local p=v*(1-s)local q=v*(1-s*f)local t=v*(1-s*(1-f))if i==0 or i==6 then +return v,t,p,a or 1.0 +elseif i==1 then +return q,v,p,a or 1.0 +elseif i==2 then +return p,v,t,a or 1.0 +elseif i==3 then +return p,q,v,a or 1.0 +elseif i==4 then +return t,p,v,a or 1.0 +elseif i==5 then +return v,p,q,a or 1.0 +else +log.error('invalid hsv (%s %s %s) i=%s', h, s, v, i)end +end +local function hue2rgb(p,q,t)if t<0 then +t=t+1 +elseif t>1 then +t=t-1 +end +if t<1/6 then +return p+(q-p)*6*t +elseif t<1/2 then +return q +elseif t<2/3 then +return p+(q-p)*(2/3-t)*6 +else +return p +end +end +function rtk.color.hsl2rgb(h,s,l,a)local r,g,b +if s==0 then +r,g,b=l,l,l +else +local q=(l<0.5)and(l*(1+s))or(l+s-l*s)local p=2*l-q +r=hue2rgb(p,q,h+1/3)g=hue2rgb(p,q,h)b=hue2rgb(p,q,h-1/3)end +return r,g,b,a or 1.0 +end +rtk.color.names={black='#000000',silver='#c0c0c0',gray='#808080',white='#ffffff',maroon='#800000',red='#ff0000',purple='#800080',fuchsia='#ff00ff',green='#008000',lime='#00ff00',olive='#808000',yellow='#ffff00',navy='#000080',blue='#0000ff',teal='#008080',aqua='#00ffff',orange='#ffa500',aliceblue='#f0f8ff',antiquewhite='#faebd7',aquamarine='#7fffd4',azure='#f0ffff',beige='#f5f5dc',bisque='#ffe4c4',blanchedalmond='#ffebcd',blueviolet='#8a2be2',brown='#a52a2a',burlywood='#deb887',cadetblue='#5f9ea0',chartreuse='#7fff00',chocolate='#d2691e',coral='#ff7f50',cornflowerblue='#6495ed',cornsilk='#fff8dc',crimson='#dc143c',cyan='#00ffff',darkblue='#00008b',darkcyan='#008b8b',darkgoldenrod='#b8860b',darkgray='#a9a9a9',darkgreen='#006400',darkgrey='#a9a9a9',darkkhaki='#bdb76b',darkmagenta='#8b008b',darkolivegreen='#556b2f',darkorange='#ff8c00',darkorchid='#9932cc',darkred='#8b0000',darksalmon='#e9967a',darkseagreen='#8fbc8f',darkslateblue='#483d8b',darkslategray='#2f4f4f',darkslategrey='#2f4f4f',darkturquoise='#00ced1',darkviolet='#9400d3',deeppink='#ff1493',deepskyblue='#00bfff',dimgray='#696969',dimgrey='#696969',dodgerblue='#1e90ff',firebrick='#b22222',floralwhite='#fffaf0',forestgreen='#228b22',gainsboro='#dcdcdc',ghostwhite='#f8f8ff',gold='#ffd700',goldenrod='#daa520',greenyellow='#adff2f',grey='#808080',honeydew='#f0fff0',hotpink='#ff69b4',indianred='#cd5c5c',indigo='#4b0082',ivory='#fffff0',khaki='#f0e68c',lavender='#e6e6fa',lavenderblush='#fff0f5',lawngreen='#7cfc00',lemonchiffon='#fffacd',lightblue='#add8e6',lightcoral='#f08080',lightcyan='#e0ffff',lightgoldenrodyellow='#fafad2',lightgray='#d3d3d3',lightgreen='#90ee90',lightgrey='#d3d3d3',lightpink='#ffb6c1',lightsalmon='#ffa07a',lightseagreen='#20b2aa',lightskyblue='#87cefa',lightslategray='#778899',lightslategrey='#778899',lightsteelblue='#b0c4de',lightyellow='#ffffe0',limegreen='#32cd32',linen='#faf0e6',magenta='#ff00ff',mediumaquamarine='#66cdaa',mediumblue='#0000cd',mediumorchid='#ba55d3',mediumpurple='#9370db',mediumseagreen='#3cb371',mediumslateblue='#7b68ee',mediumspringgreen='#00fa9a',mediumturquoise='#48d1cc',mediumvioletred='#c71585',midnightblue='#191970',mintcream='#f5fffa',mistyrose='#ffe4e1',moccasin='#ffe4b5',navajowhite='#ffdead',oldlace='#fdf5e6',olivedrab='#6b8e23',orangered='#ff4500',orchid='#da70d6',palegoldenrod='#eee8aa',palegreen='#98fb98',paleturquoise='#afeeee',palevioletred='#db7093',papayawhip='#ffefd5',peachpuff='#ffdab9',peru='#cd853f',pink='#ffc0cb',plum='#dda0dd',powderblue='#b0e0e6',rosybrown='#bc8f8f',royalblue='#4169e1',saddlebrown='#8b4513',salmon='#fa8072',sandybrown='#f4a460',seagreen='#2e8b57',seashell='#fff5ee',sienna='#a0522d',skyblue='#87ceeb',slateblue='#6a5acd',slategray='#708090',slategrey='#708090',snow='#fffafa',springgreen='#00ff7f',steelblue='#4682b4',tan='#d2b48c',thistle='#d8bfd8',tomato='#ff6347',turquoise='#40e0d0',violet='#ee82ee',wheat='#f5deb3',whitesmoke='#f5f5f5',yellowgreen='#9acd32',rebeccapurple='#663399',}end)() +__mod_rtk_font=(function() +local rtk=__mod_rtk_core +local _fontcache={}local _idmgr=rtk.IndexManager(2,127)rtk.Font=rtk.class('rtk.Font')rtk.Font.register{name=nil,size=nil,scale=nil,flags=nil,texth=nil,}function rtk.Font:initialize(name,size,scale,flags)if size then +self:set(name,size,scale,flags)end +end +function rtk.Font:finalize()if self._idx then +self:_decref()end +end +function rtk.Font:_decref()if not self._idx or self._idx==1 then +return +end +local refcount=_fontcache[self._key][2] +if refcount<=1 then +_idmgr:release(self._idx)_fontcache[self._key]=nil +else +_fontcache[self._key][2]=refcount-1 +end +end +function rtk.Font:_get_id()local idx=_idmgr:next(true)if idx then +return idx +end +return 1 +end +function rtk.Font:draw(text,x,y,clipw,cliph,flags)if rtk.os.mac then +local fudge=1*rtk.scale.value +y=y+fudge +if cliph then +cliph=cliph-fudge +end +end +flags=flags or 0 +self:set()if type(text)=='string' then +gfx.x=x +gfx.y=y +if cliph then +gfx.drawstr(text,flags,x+clipw,y+cliph)else +gfx.drawstr(text,flags)end +elseif #text==1 then +local segment,sx,sy,sw,sh=table.unpack(text[1])gfx.x=x+sx +gfx.y=y+sy +if cliph then +gfx.drawstr(segment,flags,x+clipw,y+cliph)else +gfx.drawstr(segment,flags)end +else +flags=flags|(cliph and 0 or 256)local checkh=cliph +clipw=x+(clipw or 0)cliph=y+(cliph or 0)for n=1,#text do +local segment,sx,sy,sw,sh=table.unpack(text[n])local offy=y+sy +if checkh and offy>cliph then +break +elseif offy+sh>=0 then +gfx.x=x+sx +gfx.y=offy +gfx.drawstr(segment,flags,clipw,cliph)end +end +end +end +function rtk.Font:measure(s)self:set()return gfx.measurestr(s)end +local _wrap_characters={[' '] = true,['-'] = true,[','] = true,['.'] = true,['!'] = true,['?'] = true,['\n'] = true,['/'] = true,['\\'] = true,[';'] = true,[':'] = true,}function rtk.Font:layout(text,boxw,boxh,wrap,align,relative,spacing,breakword)self:set()local segments={text=text,boxw=boxw,boxh=boxh,wrap=wrap,align=align,relative=relative,spacing=spacing,multiplier=rtk.font.multiplier,scale=rtk.scale.value,dirty=false,isvalid=function()return not self.dirty and self.scale==rtk.scale.value and self.multiplier==rtk.font.multiplier +end +}align=align or rtk.Widget.LEFT +spacing=(spacing or 0)+math.ceil((rtk.os.mac and 3 or 0)*rtk.scale.value)if not text:find('\n') then +local w,h=gfx.measurestr(text)if w<=boxw or not wrap then +segments[1]={text,0,0,w,h}return segments,w,h +end +end +local maxwidth=0 +local y=0 +local function addsegment(segment)local w,h=gfx.measurestr(segment)segments[#segments+1]={segment,0,y,w,h}maxwidth=math.max(w,maxwidth)y=y+h+spacing +end +if not wrap then +for n, line in ipairs(text:split('\n')) do +if #line>0 then +addsegment(line)else +y=y+self.texth+spacing +end +end +else +local startpos=1 +local wrappos=1 +local len=text:len()for endpos=1,len do +local substr=text:sub(startpos,endpos)local ch=text:sub(endpos,endpos)local w,h=gfx.measurestr(substr)if _wrap_characters[ch] then +wrappos=endpos +end +if w > boxw or ch=='\n' then +local wrapchar=_wrap_characters[text:sub(wrappos,wrappos)] +if breakword and(wrappos==startpos or not wrapchar)then +wrappos=endpos-1 +end +if wrappos>startpos and(breakword or wrapchar)then +addsegment(text:sub(startpos,wrappos):strip())startpos=wrappos+1 +wrappos=endpos +elseif ch=='\n' then +y=y+self.texth+spacing +end +end +end +if startpos<=len then +addsegment(string.strip(text:sub(startpos,len)))end +end +if align==rtk.Widget.CENTER then +maxwidth=relative and maxwidth or boxw +for n,segment in ipairs(segments)do +segment[2]=(maxwidth-segment[4])/2 +end +end +if align==rtk.Widget.RIGHT then +maxwidth=relative and maxwidth or boxw +for n,segment in ipairs(segments)do +segment[2]=maxwidth-segment[4] +end +end +return segments,maxwidth,y +end +function rtk.Font:set(name,size,scale,flags)local global_scale=rtk.scale.value +if not size and self._last_global_scale~=global_scale then +name=name or self.name +size=self.size +scale=scale or self.scale +flags=flags or self.flags +else +scale=scale or 1 +flags=flags or 0 +end +local sz=size and math.ceil(size*scale*global_scale*rtk.font.multiplier)local newfont=name and(name~=self.name or sz~=self.calcsize or flags~=self.flags)if self._idx and self._idx>1 then +if not newfont then +gfx.setfont(self._idx)return false +else +self:_decref()end +elseif self._idx==1 then +gfx.setfont(1,self.name,self.calcsize,self.flags)return true +end +if not newfont then +error('rtk.Font:set() called without arguments and no font parameters previously set')end +local key=name..tostring(sz)..tostring(flags)local cache=_fontcache[key] +local idx +if not cache then +idx=self:_get_id()if idx>1 then +_fontcache[key]={idx,1}end +else +cache[2]=cache[2]+1 +idx=cache[1] +end +gfx.setfont(idx,name,sz,flags)self._key=key +self._idx=idx +self._last_global_scale=global_scale +self.name=name +self.size=size +self.scale=scale +self.flags=flags +self.calcsize=sz +self.texth=gfx.texth +return true +end +end)() + +__mod_rtk_event=(function() +local rtk=__mod_rtk_core +local log=__mod_rtk_log +rtk.Event=rtk.class('rtk.Event')rtk.Event.static.MOUSEDOWN=1 +rtk.Event.static.MOUSEUP=2 +rtk.Event.static.MOUSEMOVE=3 +rtk.Event.static.MOUSEWHEEL=4 +rtk.Event.static.KEY=5 +rtk.Event.static.DROPFILE=6 +rtk.Event.static.typenames={[rtk.Event.MOUSEDOWN]='mousedown',[rtk.Event.MOUSEUP]='mouseup',[rtk.Event.MOUSEMOVE]='mousemove',[rtk.Event.MOUSEWHEEL]='mousewheel',[rtk.Event.KEY]='key',[rtk.Event.DROPFILE]='dropfile',}rtk.Event.register{type=nil,handled=nil,button=0,buttons=0,wheel=0,hwheel=0,keycode=nil,char=nil,ctrl=false,shift=false,alt=false,meta=false,modifiers=nil,files=nil,x=nil,y=nil,time=0,simulated=nil,debug=nil,}function rtk.Event:initialize(attrs)self:reset()if attrs then +table.merge(self,attrs)end +end +function rtk.Event:__tostring()local custom +if self.type>=1 and self.type<=3 then +custom = string.format(' button=%s buttons=%s', self.button, self.buttons)elseif self.type==4 then +custom = string.format(' wheel=%s,%s', self.hwheel, self.wheel)elseif self.type==5 then +custom = string.format(' char=%s keycode=%s', self.char, self.keycode)elseif self.type==6 then +custom=' ' .. table.tostring(self.files)end +return string.format('Event<%s xy=%s,%s handled=%s sim=%s%s>',rtk.Event.typenames[self.type] or 'unknown',self.x,self.y,self.handled,self.simulated,custom or '')end +function rtk.Event:reset(type)table.merge(self,self.class.attributes.defaults)self.type=type +self.handled=nil +self.debug=nil +self.files=nil +self.simulated=nil +self.time=nil +self.char=nil +self.x=gfx.mouse_x +self.y=gfx.mouse_y +return self +end +function rtk.Event:is_mouse_event()return self.type<=rtk.Event.MOUSEWHEEL +end +function rtk.Event:get_button_duration(button)local buttonstate=rtk.mouse.state[button or self.button] +if buttonstate then +return self.time-buttonstate.time +end +end +function rtk.Event:set_widget_mouseover(widget)if rtk.debug and not self.debug then +self.debug=widget +end +if widget.tooltip and not rtk._mouseover_widget and self.type==rtk.Event.MOUSEMOVE and not self.simulated then +rtk._mouseover_widget=widget +end +end +function rtk.Event:set_widget_pressed(widget)if not rtk._pressed_widgets then +rtk._pressed_widgets={order={}}end +table.insert(rtk._pressed_widgets.order,widget)rtk._pressed_widgets[widget.id]={self.x,self.y,self.time}if not rtk._drag_candidates then +rtk._drag_candidates={}end +table.insert(rtk._drag_candidates,{widget,false})end +function rtk.Event:is_widget_pressed(widget)return rtk._pressed_widgets and rtk._pressed_widgets[widget.id] and true or false +end +function rtk.Event:set_button_state(key,value)rtk.mouse.state[self.button][key]=value +end +function rtk.Event:get_button_state(key)local s=rtk.mouse.state[self.button] +return s and s[key] +end +function rtk.Event:set_modifiers(cap,button)self.modifiers=cap&(4|8|16|32)self.ctrl=cap&4~=0 +self.shift=cap&8~=0 +self.alt=cap&16~=0 +self.meta=cap&32~=0 +self.buttons=cap&(1|2|64)self.button=button +end +function rtk.Event:set_handled(widget)self.handled=widget or true +end +function rtk.Event:clone(overrides)local event=rtk.Event()for k,v in pairs(self)do +event[k]=v +end +event.handled=nil +table.merge(event,overrides or {})return event +end +end)() + +__mod_rtk_image=(function() +local rtk=__mod_rtk_core +local log=__mod_rtk_log +rtk.Image=rtk.class('rtk.Image')rtk.Image.static._icons={}rtk.Image.static.DEFAULT=0 +rtk.Image.static.ADDITIVE_BLEND=1 +rtk.Image.static.SUBTRACTIVE_BLEND=128 +rtk.Image.static.NO_SOURCE_ALPHA=2 +rtk.Image.static.NO_FILTERING=4 +rtk.Image.static.FAST_BLIT=2|4 +rtk.Image.static.ids=rtk.IndexManager(0,1023)local function _search_image_paths_list(fname,paths)if not paths or #paths==0 then +return +end +local path=string.format('%s/%s', paths[1], fname)if rtk.file.exists(path)then +return path +end +if #paths>1 then +for i=2,#paths do +path=string.format('%s/%s', paths[i], fname)if rtk.file.exists(path)then +return path +end +end +end +end +function rtk.Image.static._search_image_paths(fname,style)if not style then +local path=_search_image_paths_list(fname,rtk._image_paths)if path then +return path,true +end +style=rtk.theme.iconstyle +end +local path=_search_image_paths_list(fname,rtk._image_paths[style])if path then +return path,true +end +local otherstyle=style=='light' and 'dark' or 'light'local path=_search_image_paths_list(fname,rtk._image_paths[otherstyle])if path then +return path,false +end +end +function rtk.Image.static.icon(name,style)style=style or rtk.theme.iconstyle +local img +local pack=rtk.Image._icons[name] +if pack then +img=pack:get(name,style)if img then +return img +end +end +local path, matched=rtk.Image._search_image_paths(name .. '.png', style)if path then +img=rtk.Image():load(path)if not matched then +img:recolor(style=='light' and '#ffffff' or '#000000')end +img.style=style +end +if not img then +log.error('rtk: rtk.Image.icon("%s"): icon not found in any icon path', name)end +return img +end +rtk.Image.static.make_icon=rtk.Image.static.icon +function rtk.Image.static.make_placeholder_icon(w,h,style)local img=rtk.Image(w or 24,h or 24)img:pushdest()rtk.color.set({1,0.2,0.2,1})gfx.setfont(1, 'Sans', w or 24)gfx.x,gfx.y=5,0 +gfx.drawstr('?')img:popdest()img.style=style or 'dark'return img +end +rtk.Image.register{x=0,y=0,w=nil,h=nil,density=1.0,path=nil,rotation=0,id=nil,}function rtk.Image:initialize(w,h,density)table.merge(self,self.class.attributes.defaults)if h then +self:create(w,h,density)end +end +function rtk.Image:finalize()if self.id and not self._ref then +gfx.setimgdim(self.id,0,0)rtk.Image.static.ids:release(self.id)end +end +function rtk.Image:__tostring()local clsname=self.class.name:gsub('rtk.', '')return string.format('<%s %s,%s %sx%s id=%s density=%s path=%s ref=%s>',clsname,self.x,self.y,self.w,self.h,self.id,self.density,self.path,self._ref +)end +function rtk.Image:create(w,h,density)if not self.id then +self.id=rtk.Image.static.ids:next(true)if not self.id then +error("unable to allocate image: ran out of available REAPER image buffers")end +end +if h~=nil then +self:resize(w,h,false)end +self.density=density or 1.0 +return self +end +function rtk.Image:load(path,density)local found=path +if not rtk.file.exists(path)then +found=rtk.script_path..path +if not rtk.file.exists(found)then +found=rtk.Image._search_image_paths(path)end +end +self._path=found +local id=self.id +if not id or self._ref then +id=rtk.Image.static.ids:next()end +local res=gfx.loadimg(id,found)if res~=-1 then +self.id=id +self.path=found +self.w,self.h=gfx.getimgdim(self.id)self.density=density or 1.0 +return self +else +rtk.Image.static.ids:release(id)self.w,self.h=nil,nil +self.id=nil +log.warning('rtk: rtk.Image:load("%s"): no such file found in any search paths', path)return nil +end +end +function rtk.Image:pushdest()assert(self.id, 'create() or load() must be called first')rtk.pushdest(self.id)end +function rtk.Image:popdest()assert(gfx.dest==self.id, 'rtk.Image.popdest() called on image that is not the current drawing target')rtk.popdest(self.id)end +function rtk.Image:clone()local newimg=rtk.Image(self.w,self.h)if self.id then +newimg:blit{src=self,sx=self.x,sy=self.y}end +newimg.density=self.density +return newimg +end +function rtk.Image:resize(w,h,clear)w=math.ceil(w)h=math.ceil(h)if self.w~=w or self.h~=h then +if not self.id then +return self:create(w,h)end +self.w,self.h=w,h +gfx.setimgdim(self.id,0,0)gfx.setimgdim(self.id,w,h)end +if clear~=false then +self:clear()end +return self +end +function rtk.Image:scale(w,h,mode,density)assert(w or h, 'one or both of w or h parameters must be specified')if not self.id then +return rtk.Image(w,h)end +local aspect=self.w/self.h +w=w or(h/aspect)h=h or(w*aspect)local newimg=rtk.Image(w,h)newimg:blit{src=self,sx=self.x,sy=self.y,sw=self.w,sh=self.h,dw=newimg.w,dh=newimg.h,mode=mode}newimg.density=density or self.density +return newimg +end +function rtk.Image:clear(color)self:pushdest()if not color then +gfx.set(0,0,0,0,rtk.Image.DEFAULT,self.id,0)gfx.setimgdim(self.id,0,0)gfx.setimgdim(self.id,self.w,self.h)else +rtk.color.set(color)gfx.mode=rtk.Image.DEFAULT +end +gfx.rect(self.x,self.y,self.w,self.h,1)gfx.set(0,0,0,1,rtk.Image.DEFAULT,self.id,1)self:popdest()return self +end +function rtk.Image:viewport(x,y,w,h,density)local new=rtk.Image()new.id=self.id +new.density=density or self.density +new.path=self.path +new.x=x or 0 +new.y=y or 0 +new.w=w or(self.w-new.x)new.h=h or(self.h-new.y)new._ref=self +return new +end +function rtk.Image:draw(dx,dy,a,scale,clipw,cliph,mode)return self:blit{dx=dx,dy=dy,alpha=a,clipw=clipw,cliph=cliph,mode=mode,scale=scale +}end +function rtk.Image:blit(attrs)attrs=attrs or {}gfx.a=attrs.alpha or 1.0 +local mode=attrs.mode or rtk.Image.DEFAULT +if mode&rtk.Image.SUBTRACTIVE_BLEND~=0 then +mode=(mode&~rtk.Image.SUBTRACTIVE_BLEND)|rtk.Image.ADDITIVE_BLEND +gfx.a=-gfx.a +end +gfx.mode=mode +local src=attrs.src +if src and type(src)=='table' then +assert(rtk.isa(src, rtk.Image), 'src must be an rtk.Image or numeric image id')src=src.id +end +if src then +self:pushdest()end +local scale=(attrs.scale or 1.0)/self.density +local sx=attrs.sx or self.x +local sy=attrs.sy or self.y +local sw=attrs.sw or self.w +local sh=attrs.sh or self.h +local dx=attrs.dx or 0 +local dy=attrs.dy or 0 +local dw=attrs.dw or(sw*scale)local dh=attrs.dh or(sh*scale)if attrs.clipw and dw>attrs.clipw then +sw=sw-(dw-attrs.clipw)/(dw/sw)dw=attrs.clipw +end +if attrs.cliph and dh>attrs.cliph then +sh=sh-(dh-attrs.cliph)/(dh/sh)dh=attrs.cliph +end +if self.rotation==0 then +gfx.blit(src or self.id,1.0,0,sx,sy,sw,sh,dx or 0,dy or 0,dw,dh,0,0)else +gfx.blit(src or self.id,1.0,self.rotation,sx-(self._soffx or 0),sy-(self._soffy or 0),self._dw,self._dh,dx-(self._doffx or 0),dy-(self._doffy or 0),self._dw,self._dh,0,0 +)end +gfx.mode=0 +if src then +self:popdest()end +return self +end +function rtk.Image:recolor(color)local r,g,b,_=rtk.color.rgba(color)return self:filter(0,0,0,1.0,r,g,b,0)end +function rtk.Image:filter(mr,mg,mb,ma,ar,ag,ab,aa)self:pushdest()gfx.muladdrect(self.x,self.y,self.w,self.h,mr,mg,mb,ma,ar,ag,ab,aa)self:popdest()return self +end +function rtk.Image:rect(color,x,y,w,h,fill)self:pushdest()rtk.color.set(color)gfx.rect(x,y,w,h,fill)self:popdest()return self +end +function rtk.Image:blur(strength,x,y,w,h)if not self.w then +end +self:pushdest()gfx.mode=6 +x=x or 0 +y=y or 0 +for i=1,strength or 20 do +gfx.x=x +gfx.y=y +gfx.blurto(x+(w or self.w),y+(h or self.h))end +self:popdest()return self +end +function rtk.Image:flip_vertical()self:pushdest()gfx.mode=6 +gfx.a=1 +gfx.transformblit(self.id,self.x,self.y,self.w,self.h,2,2,{self.x,self.y+self.h,self.x+self.w,self.y+self.h,self.x,self.y,self.x+self.w,self.y +})rtk.popdest()return self +end +local function _xlate(x,y,theta)return x*math.cos(theta)-y*math.sin(theta),x*math.sin(theta)+y*math.cos(theta)end +function rtk.Image:rotate(degrees)self.rotation=math.rad(degrees)local x1,y1=0,0 +local xt1,yt1=_xlate(x1,y1,self.rotation)local x2,y2=0+self.w,0 +local xt2,yt2=_xlate(x2,y2,self.rotation)local x3,y3=0,self.h +local xt3,yt3=_xlate(x3,y3,self.rotation)local x4,y4=0+self.w,self.h +local xt4,yt4=_xlate(x4,y4,self.rotation)local xmin=math.min(xt1,xt2,xt3,xt4)local xmax=math.max(xt1,xt2,xt3,xt4)local ymin=math.min(yt1,yt2,yt3,yt4)local ymax=math.max(yt1,yt2,yt3,yt4)local dw=xmax-xmin +local dh=ymax-ymin +local dmax=math.max(dw,dh)self._dw=dmax +self._dh=dmax +self._soffx=(dmax-self.w)/2 +self._soffy=(dmax-self.h)/2 +self._doffx=math.max(0,(dh-dw)/2)self._doffy=math.max(0,(dw-dh)/2)return self +end +function rtk.Image:refresh_scale()end +end)() + +__mod_rtk_multiimage=(function() +local rtk=__mod_rtk_core +local log=__mod_rtk_log +rtk.MultiImage=rtk.class('rtk.MultiImage', rtk.Image)function rtk.MultiImage:initialize(...)rtk.Image.initialize(self)self._variants={}local images={...}for _,img in ipairs(images)do +self:add(img)end +end +function rtk.MultiImage:finalize()end +function rtk.MultiImage:add(path_or_image,density)local img +if rtk.isa(path_or_image,rtk.Image)then +assert(not rtk.isa(path_or_image, rtk.MultiImage), 'cannot add an rtk.MultiImage to an rtk.MultiImage')img=path_or_image +else +assert(density, 'density must be supplied when path is passed to add()')img=rtk.Image:load(path_or_image,density)end +assert(not self._variants[img.density], 'replacing existing density not supported')self._variants[img.density]=img +if not self.id or self.density==img.density then +self:_set(img)end +if not self._max or img.density>self._max.density then +self._max=img +end +return img +end +function rtk.MultiImage:load(path,density)if self:add(path,density)then +return self +end +end +function rtk.MultiImage:_set(img)self.current=img +self.id=img.id +self.x=img.x +self.y=img.y +self.w=img.w +self.h=img.h +self.density=img.density +self.path=img.path +self.rotation=img.rotation +end +function rtk.MultiImage:refresh_scale(scale)local best=self._max +scale=scale or rtk.scale.value +for density,img in pairs(self._variants)do +if density==scale then +best=img +break +elseif density>scale and density 0, 'no strips provided (either as a "strips" field or as positional elements elements)')local src_idx=#self._sources+1 +self._sources[src_idx]={src=attrs.src,recolors={}}local y=0 +for _,strip in ipairs(strips)do +assert(type(strip)=='table', 'ImagePack strip definition must be a table')assert(type(strip.w) == 'number' or type(strip.h) == 'number', 'ImagePack strip requires either "w" or "h" fields')local names=strip.names or attrs.names +assert(type(names)=='table', 'ImagePack strip missing "names" field or is not table')local sizes=strip.sizes +if not sizes then +local density=strip.density or attrs.density or 1 +if strip.size then +sizes={{strip.size,density}}elseif attrs.sizes then +sizes=attrs.sizes +elseif attrs.size then +sizes={{attrs.size,density}}else +sizes={{self.default_size,density}}end +end +strip.w=strip.w or strip.h +strip.h=strip.h or strip.w +local columns=strip.columns or attrs.columns +local rowwidth=columns and(columns*strip.w)local style=strip.style or attrs.style +local x=0 +for _,name in ipairs(names)do +local subregion={id=self._last_id,src_idx=src_idx,x=x,y=y,w=strip.w,h=strip.h,}self._last_id=self._last_id+1 +for _,sizedensity in ipairs(sizes)do +local size,density=table.unpack(sizedensity)local key=string.format('%s:%s:%s', style, name, size)local densities=self._regions[key] +if not densities then +densities={}self._regions[key]=densities +elseif densities[density] then +error(string.format('duplicate image name "%s" for style=%s size=%s density=%s',name,style,size,density +))end +densities[density]=subregion +end +x=x+strip.w +if rowwidth and x>=rowwidth then +x=0 +y=y+strip.h +end +end +y=y+strip.h +end +return self +end +function rtk.ImagePack:_get_densities(name,style)local key +if not name:find(':') then +key=string.format('%s:%s:%s', style, name, self.default_size)else +key=string.format('%s:%s', style, name)end +return key,self._regions[key] +end +function rtk.ImagePack:get(name,style)if not name then +return +end +local key,densities=self:_get_densities(name,style)local multi=self._cache[key] +if multi then +return multi +end +local recolor=false +if not densities and not style then +style=rtk.theme.iconstyle +densities=self:_get_densities(name,style)end +if not densities and style then +local otherstyle=style=='light' and 'dark' or 'light'recolor=true +_,densities=self:_get_densities(name,otherstyle)if not densities then +_,densities=self:_get_densities(name,nil)recolor=true +end +end +if not densities then +return +end +local multi=rtk.MultiImage()for density,region in pairs(densities)do +local src=self._sources[region.src_idx] +local img=src.img +if not img then +img=rtk.Image():load(src.src)src.img=img +end +if recolor then +img=src.recolors[style] +if not img then +img=src.img:clone():recolor(style=='light' and '#ffffff' or '#000000')src.recolors[style]=img +end +end +assert(img, string.format('could not read "%s"', src.src))multi:add(img:viewport(region.x,region.y,region.w,region.h,density))end +multi.style=style +self._cache[key]=multi +return multi +end +function rtk.ImagePack:register_as_icons()local default_size=self.default_size +for key,_ in pairs(self._regions)do +local idx=key:find(':')local name=key:sub(idx+1)rtk.Image._icons[name]=self +idx=name:find(':')local size=name:sub(idx+1)if size==default_size then +name=name:sub(1,idx-1)rtk.Image._icons[name]=self +end +end +return self +end +end)() + +__mod_rtk_shadow=(function() +local rtk=__mod_rtk_core +rtk.Shadow=rtk.class('rtk.Shadow')rtk.Shadow.static.RECTANGLE=0 +rtk.Shadow.static.CIRCLE=1 +rtk.Shadow.register{type=nil,color='#00000055',w=nil,h=nil,radius=nil,elevation=nil,}function rtk.Shadow:initialize(color)self.color=color or self.class.attributes.color.default +self._image=nil +self._last_draw_params=nil +end +function rtk.Shadow:set_rectangle(w,h,elevation,t,r,b,l)self.type=rtk.Shadow.RECTANGLE +self.w=w +self.h=h +self.tt=t or elevation +self.tr=r or elevation +self.tb=b or elevation +self.tl=l or elevation +assert(self.tt or self.tr or self.tb or self.tl, 'missing elevation for at least one edge')self.elevation=elevation or math.max(self.tt,self.tr,self.tb,self.tl)self.radius=nil +self._check_generate=true +end +function rtk.Shadow:set_circle(radius,elevation)self.type=rtk.Shadow.CIRCLE +elevation=elevation or radius/1.5 +if self.radius==radius and self.elevation==elevation then +return +end +self.radius=radius +self.elevation=elevation +self._check_generate=true +end +function rtk.Shadow:draw(x,y,alpha)if self.radius then +self:_draw_circle(x,y,alpha or 1.0)else +self:_draw_rectangle(x,y,alpha or 1.0)end +end +function rtk.Shadow:_needs_generate()if self._check_generate==false then +return false +end +local params=self._last_draw_params +local gen=not params or +self.w~=params.w or +self.h~=params.h or +self.tt~=params.tt or +self.tr~=params.tr or +self.tb~=params.tb or +self.tl~=params.tl or +self.elevation~=params.elevation or +self.radius~=params.radius +if gen then +self._last_draw_params={w=self.w,h=self.h,tt=self.tt,tr=self.tr,tb=self.tb,tl=self.tl,elevation=self.elevation,radius=self.radius +}end +self._check_generate=false +return gen +end +function rtk.Shadow:_draw_circle(x,y,alpha)local pad=self.elevation*3 +if self:_needs_generate()then +local radius=math.ceil(self.radius)local sz=(radius+2+pad)*2 +if not self._image then +self._image=rtk.Image(sz,sz)else +self._image:resize(sz,sz,true)end +self._image:pushdest()rtk.color.set(self.color)local a=0.65-0.5*(1-1/self.elevation)local inflection=radius +local origin=-math.log(1/(pad))for i=radius+pad,1,-1 do +if i>inflection then +gfx.a2=-math.log((i-inflection)/(pad))/origin*a +else +end +gfx.circle(pad+radius,pad+radius,i,1,1)end +gfx.a2=1 +gfx.set(0,0,0,1)self._image:popdest()self._needs_draw=false +end +self._image:draw(x-pad,y-pad,alpha)end +function rtk.Shadow:_draw_rectangle(x,y,alpha)local tt,tr,tb,tl=self.tt,self.tr,self.tb,self.tl +local pad=math.max(tl,tr,tt,tb)if self:_needs_generate()then +local w=self.w+(tl+tr)+pad*2 +local h=self.h+(tt+tb)+pad*2 +if not self._image then +self._image=rtk.Image(w,h)else +self._image:resize(w,h,true)end +self._image:pushdest()rtk.color.set(self.color)local a=gfx.a +gfx.a=1 +for i=0,pad do +gfx.a2=a*(i+1)/pad +rtk.gfx.roundrect(pad+i,pad+i,self.w+tl+tr-i*2,self.h+tt+tb-i*2,self.elevation,0)end +self._image:popdest()self._needs_draw=false +end +if tr>0 then +self._image:blit{sx=pad+tl+self.w,sw=tr+pad,sh=h,dx=x+self.w,dy=y-tt-pad,alpha=alpha +}end +if tb>0 then +self._image:blit{sy=pad+tt+self.h,sw=self.w+tl+pad,sh=tb+pad,dx=x-tl-pad,dy=y+self.h,alpha=alpha +}end +if tt>0 then +self._image:blit{sx=0,sy=0,sw=self.w+tl+pad,sh=pad+tt,dx=x-tl-pad,dy=y-tt-pad,alpha=alpha +}end +if tl>0 then +self._image:blit{sx=0,sy=pad+tt,sw=pad+tl,sh=self.h,dx=x-tl-pad,dy=y,alpha=alpha +}end +end +end)() + +__mod_rtk_nativemenu=(function() +local rtk=__mod_rtk_core +rtk.NativeMenu=rtk.class('rtk.NativeMenu')rtk.NativeMenu.static.SEPARATOR=0 +function rtk.NativeMenu:initialize(menu)self._menustr=nil +if menu then +self:set(menu)end +end +function rtk.NativeMenu:set(menu)self.menu=menu +if menu then +self:_parse()end +end +function rtk.NativeMenu:_parse(submenu)self._item_by_idx={}self._item_by_id={}self._order=self:_parse_submenu(self.menu)end +function rtk.NativeMenu:_parse_submenu(submenu,baseitem)local order=baseitem or {}for n,menuitem in ipairs(submenu)do +if type(menuitem) ~='table' then +menuitem={label=menuitem}else +menuitem=table.shallow_copy(menuitem)if not menuitem.label then +menuitem.label=table.remove(menuitem,1)end +end +if menuitem.submenu then +menuitem=self:_parse_submenu(menuitem.submenu,menuitem)menuitem.submenu=nil +elseif menuitem.label~=rtk.NativeMenu.SEPARATOR then +local idx=#self._item_by_idx+1 +menuitem.index=idx +self._item_by_idx[idx]=menuitem +end +if menuitem.id then +self._item_by_id[tostring(menuitem.id)]=menuitem +end +order[#order+1]=menuitem +end +return order +end +local function _get_item_attr(item,attr)local val=item[attr] +if type(val)=='function' then +return val()else +return val +end +end +function rtk.NativeMenu:_build_menustr(submenu,items)items=items or {}local menustr=''for n,item in ipairs(submenu)do +if not _get_item_attr(item, 'hidden') then +local flags=''if _get_item_attr(item, 'disabled') then +flags=flags .. '#'end +if _get_item_attr(item, 'checked') then +flags=flags .. '!'end +if item.label==rtk.NativeMenu.SEPARATOR then +menustr=menustr .. '|'elseif #item>0 then +menustr=menustr .. flags .. '>' .. item.label .. '|' .. self:_build_menustr(item, items) .. '<|'else +items[#items+1]=item +menustr=menustr .. flags .. item.label .. '|'end +end +end +return menustr,items +end +function rtk.NativeMenu:item(idx_or_id)if not idx_or_id or not self._item_by_idx then +return nil +end +local item=self._item_by_id[tostring(idx_or_id)] or self._item_by_id[idx_or_id] +if item then +return item +end +return self._item_by_idx[idx_or_id] +end +function rtk.NativeMenu:items()if not self._item_by_idx then +return function()end +end +local i=0 +local n=#self._item_by_idx +return function()i=i+1 +if i<=n then +return self._item_by_idx[i] +end +end +end +function rtk.NativeMenu:open(x,y)rtk.window:request_mouse_cursor(rtk.mouse.cursors.POINTER)assert(self.menu, 'menu must be set before open()')if not self._order then +self:_parse()end +local menustr,items=self:_build_menustr(self._order)local future=rtk.Future()rtk.defer(function()gfx.x=x +gfx.y=y +local choice=gfx.showmenu(menustr)local item +if choice>0 then +item=items[tonumber(choice)] +end +rtk._drag_candidates=nil +rtk.window:queue_mouse_refresh()future:resolve(item)end)return future +end +function rtk.NativeMenu:open_at_mouse()return self:open(gfx.mouse_x,gfx.mouse_y)end +function rtk.NativeMenu:open_at_widget(widget,halign,valign)assert(widget.drawn, "rtk.NativeMenu.open_at_widget() called before widget was drawn")local x=widget.clientx +local y=widget.clienty +if halign=='right' then +x=x+widget.calc.w +end +if valign ~='top' then +y=y+widget.calc.h +end +return self:open(x,y)end +end)() + +__mod_rtk_widget=(function() +local rtk=__mod_rtk_core +local log=__mod_rtk_log +rtk.Widget=rtk.class('rtk.Widget')rtk.Widget.static.LEFT=0 +rtk.Widget.static.TOP=0 +rtk.Widget.static.CENTER=1 +rtk.Widget.static.RIGHT=2 +rtk.Widget.static.BOTTOM=2 +rtk.Widget.static.POSITION_INFLOW=0x01 +rtk.Widget.static.POSITION_FIXED=0x02 +rtk.Widget.static.RELATIVE=rtk.Widget.POSITION_INFLOW|0x10 +rtk.Widget.static.ABSOLUTE=0x20 +rtk.Widget.static.FIXED=rtk.Widget.POSITION_FIXED|0x40 +rtk.Widget.static.FIXED_FLOW=rtk.Widget.POSITION_INFLOW|rtk.Widget.POSITION_FIXED|0x80 +rtk.Widget.static.BOX=1 +rtk.Widget.static.FULL=rtk.Widget.BOX|2 +rtk.Widget.static.REFLOW_DEFAULT=nil +rtk.Widget.static.REFLOW_NONE=0 +rtk.Widget.static.REFLOW_PARTIAL=1 +rtk.Widget.static.REFLOW_FULL=2 +rtk.Widget.static._calc_border=function(self,value)if type(value)=='string' then +local parts=string.split(value)if #parts==1 then +return {{rtk.color.rgba(parts[1])},1}elseif #parts==2 then +local width=parts[1]:gsub('px', '')return {{rtk.color.rgba(parts[2])},tonumber(width)}else +error('invalid border format')end +elseif value then +assert(type(value)=='table', 'border must be string or table')if #value==1 then +return {rtk.color.rgba({value[1]}),1}elseif #value==2 then +return value +elseif #value==4 then +return {value,1}else +log.exception('invalid border value: %s', table.tostring(value))error('invalid border value')end +end +end +rtk.Widget.static._calc_padding_or_margin=function(value)if not value then +return 0,0,0,0 +elseif type(value)=='number' then +return value,value,value,value +else +if type(value)=='string' then +local parts=string.split(value)value={}for i=1,#parts do +local sz=parts[i]:gsub('px', '')value[#value+1]=tonumber(sz)end +end +if #value==1 then +return value[1],value[1],value[1],value[1] +elseif #value==2 then +return value[1],value[2],value[1],value[2] +elseif #value==3 then +return value[1],value[2],value[3],value[2] +elseif #value==4 then +return value[1],value[2],value[3],value[4] +else +error('invalid value')end +end +end +rtk.Widget.register{x=rtk.Attribute{default=0,reflow=rtk.Widget.REFLOW_FULL},y=rtk.Attribute{default=0,reflow=rtk.Widget.REFLOW_FULL},w=rtk.Attribute{type='number',reflow=rtk.Widget.REFLOW_FULL,animate=function(self,anim)local val=anim.resolve(anim.easingfunc(anim.pct))return(anim.dst>1.0 and val>0 and val<=1.0)and 1.01 or val,val==anim.dst +end +},h=rtk.Attribute{type='number',reflow=rtk.Widget.REFLOW_FULL,animate=rtk.Reference('w'),},z=rtk.Attribute{default=0,reflow=rtk.Widget.REFLOW_FULL},minw = rtk.Attribute{type='number', reflow=rtk.Widget.REFLOW_FULL},minh = rtk.Attribute{type='number', reflow=rtk.Widget.REFLOW_FULL},maxw = rtk.Attribute{type='number', reflow=rtk.Widget.REFLOW_FULL},maxh = rtk.Attribute{type='number', reflow=rtk.Widget.REFLOW_FULL},halign=rtk.Attribute{default=rtk.Widget.LEFT,calculate={left=rtk.Widget.LEFT,center=rtk.Widget.CENTER,right=rtk.Widget.RIGHT},},valign=rtk.Attribute{default=rtk.Widget.TOP,calculate={top=rtk.Widget.TOP,center=rtk.Widget.CENTER,bottom=rtk.Widget.BOTTOM},},scalability=rtk.Attribute{default=rtk.Widget.FULL,reflow=rtk.Widget.REFLOW_FULL,calculate={box=rtk.Widget.BOX,full=rtk.Widget.FULL},},position=rtk.Attribute{default=rtk.Widget.RELATIVE,reflow=rtk.Widget.REFLOW_FULL,calculate={relative=rtk.Widget.RELATIVE,absolute=rtk.Widget.ABSOLUTE,fixed=rtk.Widget.FIXED,['fixed-flow']=rtk.Widget.FIXED_FLOW +},},box=nil,offx=nil,offy=nil,clientx=nil,clienty=nil,padding=rtk.Attribute{replaces={'tpadding', 'rpadding', 'bpadding', 'lpadding'},get=function(self,attr,target)return {target.tpadding,target.rpadding,target.bpadding,target.lpadding}end,reflow=rtk.Widget.REFLOW_FULL,calculate=function(self,attr,value,target)local t,r,b,l=rtk.Widget.static._calc_padding_or_margin(value)target.tpadding,target.rpadding,target.bpadding,target.lpadding=t,r,b,l +return {t,r,b,l}end +},tpadding=rtk.Attribute{priority=true,reflow=rtk.Widget.REFLOW_FULL},rpadding=rtk.Reference('tpadding'),bpadding=rtk.Reference('tpadding'),lpadding=rtk.Reference('tpadding'),margin=rtk.Attribute{default=0,replaces={'tmargin', 'rmargin', 'bmargin', 'lmargin'},get=function(self,attr,target)return {target.tmargin,target.rmargin,target.bmargin,target.lmargin}end,reflow=rtk.Widget.REFLOW_FULL,calculate=function(self,attr,value,target)local t,r,b,l=rtk.Widget.static._calc_padding_or_margin(value)target.tmargin,target.rmargin,target.bmargin,target.lmargin=t,r,b,l +return {t,r,b,l}end +},tmargin=rtk.Attribute{priority=true,reflow=rtk.Widget.REFLOW_FULL},rmargin=rtk.Reference('tmargin'),bmargin=rtk.Reference('tmargin'),lmargin=rtk.Reference('tmargin'),border=rtk.Attribute{reflow=rtk.Widget.REFLOW_FULL,calculate=function(self,attr,value,target)local border=rtk.Widget.static._calc_border(self,value)target.tborder=border +target.rborder=border +target.bborder=border +target.lborder=border +target.border_uniform=true +return border +end +},tborder=rtk.Attribute{priority=true,reflow=rtk.Widget.REFLOW_FULL,calculate=function(self,attr,value,target)target.border_uniform=false +return rtk.Widget.static._calc_border(self,value)end,},rborder=rtk.Reference('tborder'),bborder=rtk.Reference('tborder'),lborder=rtk.Reference('tborder'),visible=rtk.Attribute{default=true,reflow=rtk.Widget.REFLOW_FULL},disabled=false,ghost=false,tooltip=nil,cursor=nil,alpha=rtk.Attribute{default=1.0,reflow=rtk.Widget.REFLOW_NONE,},autofocus=false,bg=rtk.Attribute{calculate=function(self,attr,value,target)return value and {rtk.color.rgba(value)}end,},scroll_on_drag=true,show_scrollbar_on_drag=true,touch_activate_delay=nil,realized=false,drawn=false,viewport=nil,window=nil,mouseover=false,hovering=false,debug=nil,id=nil,ref=nil,refs=nil,}rtk.Widget.static.last_index=0 +function rtk.Widget:__allocate()self.__id=tostring(rtk.Widget.static.last_index)rtk.Widget.static.last_index=rtk.Widget.static.last_index+1 +end +function rtk.Widget:initialize(attrs,...)self.refs={__empty=true}setmetatable(self.refs,{__mode='v',__index=function(table,key)return self:_ref(table,key)end,__newindex=function(table,key,value)rawset(table,key,value)table.__empty=false +end +})self.calc={border_uniform=true +}setmetatable(self.calc,{__call=function(_,_,attr,instant)return self:_calc(attr,instant)end +})local tables={self.class.attributes.defaults,...}local merged={}for n=1,#tables do +table.merge(merged,tables[n])end +if attrs then +for k,v in pairs(attrs)do +local meta=self.class.attributes.get(k)local attr=meta.alias +if attr then +merged[attr]=v +end +local replaces=meta.replaces +if replaces then +for n=1,#replaces do +merged[replaces[n]]=nil +end +end +end +table.merge(merged,attrs)if attrs.ref then +rtk._refs[attrs.ref]=self +self.refs[attrs.ref]=self +end +end +self.id=self.__id +self:_setattrs(merged)self._last_mousedown_time=0 +self._last_reflow_scale=nil +end +function rtk.Widget:__tostring()local clsname=self.class.name:gsub('rtk.', '')if not self.calc then +return string.format('<%s (uninitialized)>', clsname)end +local info=self:__tostring_info()info=info and string.format('<%s>', info) or ''return string.format('%s%s[%s] (%s,%s %sx%s)',clsname,info,self.id,self.calc.x,self.calc.y,self.calc.w,self.calc.h +)end +function rtk.Widget:__tostring_info()end +function rtk.Widget:_setattrs(attrs)if not attrs then +return +end +local get=self.class.attributes.get +local priority={}local calc=self.calc +for k,v in pairs(attrs)do +if not tonumber(k)then +local meta=get(k)if not meta.priority then +if v==rtk.Attribute.FUNCTION then +v=self.class.attributes[k].default_func(self,k)elseif v==rtk.Attribute.NIL then +v=nil +end +local calculated=self:_calc_attr(k,v,nil,meta)self:_set_calc_attr(k,v,calculated,calc,meta)else +priority[#priority+1]=k +end +self[k]=v +end +end +for _,k in ipairs(priority)do +local v=self[k] +if v==rtk.Attribute.FUNCTION then +v=self.class.attributes[k].default_func(self,k)self[k]=v +end +if v~=nil then +if v==rtk.Attribute.NIL then +v=nil +self[k]=nil +end +local calculated=self:_calc_attr(k,v)self:_set_calc_attr(k,v,calculated,calc)end +end +end +function rtk.Widget:_ref(table,key)if self.parent then +return self.parent.refs[key] +else +return rtk._refs[key] +end +end +function rtk.Widget:_get_debug_color()if not self.debug_color then +local function hashint(i,seed)math.randomseed(i*(seed*53))return math.random(40,235)/255.0 +end +local id=self.id:hash()self.debug_color={hashint(id,1),hashint(id,2),hashint(id,3),}end +return self.debug_color +end +function rtk.Widget:_draw_debug_box(offx,offy,event)local calc=self.calc +if not self.debug and not rtk.debug or not calc.w then +return false +end +if not self.debug and event.debug~=self then +return false +end +local color=self:_get_debug_color()gfx.set(color[1],color[2],color[3],0.2)local x=calc.x+offx +local y=calc.y+offy +gfx.rect(x,y,calc.w,calc.h,1)gfx.set(color[1],color[2],color[3],0.4)gfx.rect(x,y,calc.w,calc.h,0)local tp,rp,bp,lp=self:_get_padding_and_border()if tp>0 or rp>0 or bp>0 or lp>0 then +gfx.set(color[1],color[2],color[3],0.8)gfx.rect(x+lp,y+tp,calc.w-lp-rp,calc.h-tp-bp,0)end +return true +end +function rtk.Widget:_draw_debug_info(event)local calc=self.calc +local parts={{ 15, "#6e2e2e", tostring(self.class.name:gsub("rtk.", "")) },{ 15, "#378b48", string.format('#%s', self.id) },{ 17, "#cccccc", " | " },{ 15, "#555555", string.format("%.1f", calc.x) },{ 15, "#777777", " , " },{ 15, "#555555", string.format("%.1f", calc.y) },{ 17, "#cccccc", " | " },{ 15, "#555555", string.format("%.1f", calc.w) },{ 13, "#777777", " x " },{ 15, "#555555", string.format("%.1f", calc.h) },}local sizes={}local bw,bh=0,0 +for n,part in ipairs(parts)do +local sz,_,str=table.unpack(part)gfx.setfont(1,rtk.theme.default_font,sz)local w,h=gfx.measurestr(str)sizes[n]={w,h}bw=bw+w +bh=math.max(bh,h)end +bw=bw+20 +bh=bh+10 +local x=self.clientx +local y=self.clienty +if x+bw>self.window.w then +x=self.window.w-bw +elseif x<0 then +x=0 +end +if y-bh>=0 then +y=math.max(0,y-bh)else +y=math.min(y+calc.h,self.window.calc.h-bh)end +rtk.color.set('#ffffff')gfx.rect(x,y,bw,bh,1)rtk.color.set('#777777')gfx.rect(x,y,bw,bh,0)gfx.x=x+10 +for n,part in ipairs(parts)do +local sz,color,str=table.unpack(part)rtk.color.set(color)gfx.y=y+(bh-sizes[n][2])/2 +gfx.setfont(1,rtk.theme.default_font,sz)gfx.drawstr(str)end +end +function rtk.Widget:attr(attr,value,trigger,reflow)return self:_attr(attr,value,trigger,reflow,nil,false)end +function rtk.Widget:sync(attr,value,calculated,trigger,reflow)return self:_attr(attr,value,trigger,reflow,calculated,true)end +function rtk.Widget:_attr(attr,value,trigger,reflow,calculated,sync)local meta=self.class.attributes.get(attr)if value==rtk.Attribute.DEFAULT then +if meta.default==rtk.Attribute.FUNCTION then +value=meta.default_func(self,attr)else +value=meta.default +end +elseif value==rtk.Attribute.NIL then +value=nil +end +local oldval=self[attr] +local oldcalc=self.calc[attr] +local replaces=meta.replaces +if replaces then +for i=1,#replaces do +self[replaces[i]]=nil +end +end +if calculated==nil then +calculated=self:_calc_attr(attr,value,nil,meta)end +if not rawequal(value,oldval)or calculated~=oldcalc or replaces or trigger then +self[attr]=value +self:_set_calc_attr(attr,value,calculated,self.calc,meta)self:_handle_attr(attr,calculated,oldcalc,trigger==nil or trigger,reflow,sync)end +return self +end +function rtk.Widget:_calc_attr(attr,value,target,meta,namespace,widget)target=target or self.calc +meta=meta or self.class.attributes.get(attr)if meta.type then +value=meta.type(value)end +local calculate=meta.calculate +if calculate then +local tp=type(calculate)if tp=='table' then +if value==nil then +value=calculate[rtk.Attribute.NIL] +else +value=calculate[value] or value +end +elseif tp=='function' then +if value==rtk.Attribute.NIL then +value=nil +end +value=calculate(self,attr,value,target)end +end +return value +end +function rtk.Widget:_set_calc_attr(attr,value,calculated,target,meta)meta=meta or self.class.attributes.get(attr)if meta.set then +meta.set(self,attr,value,calculated,target)else +self.calc[attr]=calculated +end +end +function rtk.Widget:_calc(attr,instant)if not instant then +local anim=self:get_animation(attr)if anim and anim.dst then +return anim.dst +end +end +local meta=self.class.attributes.get(attr)if meta.get then +return meta.get(self,attr,self.calc)else +return self.calc[attr] +end +end +function rtk.Widget:move(x,y)self:attr('x', x)self:attr('y', y)return self +end +function rtk.Widget:resize(w,h)self:attr('w', w)self:attr('h', h)return self +end +function rtk.Widget:_get_relative_pos_to_viewport()local x,y=0,0 +local widget=self +while widget do +x=x+widget.calc.x +y=y+widget.calc.y +if widget.viewport and widget.viewport==widget.parent then +break +end +widget=widget.parent +end +return x,y +end +function rtk.Widget:scrolltoview(margin,allowh,allowv,smooth)if not self.visible or not self.box or not self.viewport then +return self +end +local calc=self.calc +local vcalc=self.viewport.calc +local tmargin,rmargin,bmargin,lmargin=rtk.Widget.static._calc_padding_or_margin(margin or 0)local left,top=nil,nil +local absx,absy=self:_get_relative_pos_to_viewport()if allowh~=false then +if absx-lmarginself.viewport.scroll_left+vcalc.w then +left=absx+calc.w+rmargin-vcalc.w +end +end +if allowv~=false then +if absy-tmarginself.viewport.scroll_top+vcalc.h then +top=absy+calc.h+bmargin-vcalc.h +end +end +self.viewport:scrollto(left,top,smooth)return self +end +function rtk.Widget:hide()if self.calc.visible~=false then +return self:attr('visible', false)end +return self +end +function rtk.Widget:show()if self.calc.visible~=true then +return self:attr('visible', true)end +return self +end +function rtk.Widget:toggle()if self.calc.visible==true then +return self:hide()else +return self:show()end +end +function rtk.Widget:focused()return rtk.focused==self +end +function rtk.Widget:focus(event)if rtk.focused and rtk.focused~=self then +rtk.focused:blur(event,self)end +if rtk.focused==nil and self:_handle_focus(event)~=false then +rtk.focused=self +self:queue_draw()return true +end +return false +end +function rtk.Widget:blur(event,other)if not self:focused()then +return true +end +if self:_handle_blur(event,other)~=false then +rtk.focused=nil +self:queue_draw()return true +end +return false +end +function rtk.Widget:animate(kwargs)assert(kwargs and (kwargs.attr or #kwargs > 0), 'missing animation arguments')local calc=self.calc +local attr=kwargs.attr or kwargs[1] +local meta=self.class.attributes.get(attr)local key=string.format('%s.%s', self.id, attr)local curanim=rtk._animations[key] +local curdst=curanim and curanim.dst or self.calc[attr] +if curdst == kwargs.dst and not meta.calculate and attr ~= 'w' and attr ~= 'h' then +if curanim then +return curanim.future +elseif not kwargs.src then +return rtk.Future():resolve(self)end +end +kwargs.attr=attr +kwargs.key=key +kwargs.widget=self +kwargs.attrmeta=meta +kwargs.stepfunc=meta.animate +kwargs.calculate=meta.calculate +if kwargs.dst==rtk.Attribute.DEFAULT then +if meta.default==rtk.Attribute.FUNCTION then +kwargs.dst=meta.default_func(self,attr)else +kwargs.dst=meta.default +end +end +local doneval=kwargs.dst or rtk.Attribute.DEFAULT +if attr == 'w' or attr == 'h' then +kwargs.sync_surface_value=true +if not kwargs.src or kwargs.src<=1.0 then +kwargs.src=((attr=='w') and calc.w or calc.h or 0) * (kwargs.src or 1)end +if not kwargs.dst or kwargs.dst<=1.0 then +local current=self[attr] +self[attr]=kwargs.dst +local window=self:_slow_get_window()if not window then +return rtk.Future():resolve(self)end +window:reflow(rtk.Widget.REFLOW_FULL)kwargs.dst=(calc[attr] or 0)/rtk.scale.value +self[attr]=current +window:reflow(rtk.Widget.REFLOW_FULL)end +else +if meta.calculate then +kwargs.dst=meta.calculate(self,attr,kwargs.dst,{})doneval=kwargs.dst or rtk.Attribute.DEFAULT +end +end +if curdst==kwargs.dst then +if curanim then +return curanim.future +elseif not kwargs.src then +return rtk.Future():resolve(self)end +end +if kwargs.doneval==nil then +kwargs.doneval=doneval +end +if not kwargs.src then +kwargs.src=self:calc(attr,true)elseif meta.calculate then +kwargs.src=meta.calculate(self,attr,kwargs.src,{})end +return rtk.queue_animation(kwargs)end +function rtk.Widget:cancel_animation(attr)local anim=self:get_animation(attr)if anim then +anim.future:cancel()end +return anim +end +function rtk.Widget:get_animation(attr)local key=self.id .. '.' .. attr +return rtk._animations[key] +end +function rtk.Widget:setcolor(color,amul)rtk.color.set(color,(amul or 1)*self.calc.alpha)return self +end +function rtk.Widget:queue_draw()if self.window then +self.window:queue_draw()end +return self +end +function rtk.Widget:queue_reflow(mode,widget)local window=self:_slow_get_window()if window then +window:queue_reflow(mode,widget or self)end +return self +end +function rtk.Widget:reflow(boxx,boxy,boxw,boxh,fillw,fillh,clampw,clamph,uiscale,viewport,window)local expw,exph +if not boxx then +if self.box then +expw,exph=self:_reflow(table.unpack(self.box))else +return +end +else +self.viewport=viewport +self.window=window +self.box={boxx,boxy,boxw,boxh,fillw,fillh,clampw,clamph,uiscale,viewport,window}expw,exph=self:_reflow(boxx,boxy,boxw,boxh,fillw,fillh,clampw,clamph,uiscale,viewport,window)end +local calc=self.calc +self:onreflow()self.realized=true +return calc.x,calc.y,calc.w,calc.h,expw or fillw,exph or fillh +end +function rtk.Widget:_get_padding()local calc=self.calc +local scale=rtk.scale.value +return +(calc.tpadding or 0)*scale,(calc.rpadding or 0)*scale,(calc.bpadding or 0)*scale,(calc.lpadding or 0)*scale +end +function rtk.Widget:_get_border_sizes()local calc=self.calc +return +calc.tborder and calc.tborder[2] or 0,calc.rborder and calc.rborder[2] or 0,calc.bborder and calc.bborder[2] or 0,calc.lborder and calc.lborder[2] or 0 +end +function rtk.Widget:_get_padding_and_border()local tp,rp,bp,lp=self:_get_padding()local tb,rb,bb,lb=self:_get_border_sizes()return tp+tb,rp+rb,bp+bb,lp+lb +end +function rtk.Widget:_get_box_pos(boxx,boxy)if self.calc.scalability&rtk.Widget.FULL==rtk.Widget.FULL then +local scale=rtk.scale.value +return scale*self.x+boxx,scale*self.y+boxy +else +return self.x+boxx,self.y+boxy +end +end +local function _get_content_dimension(size,bounds,padding,fill,clamp,flags,scale)scale=flags&rtk.Widget.FULL==rtk.Widget.FULL and(rtk.scale.value*(scale or 1))or scale or 1 +if size then +if bounds and size<-1 then +return bounds+(size*scale)-padding +elseif bounds and size<=1.0 then +return math.abs(bounds*size)-padding +else +return(size*scale)-padding +end +end +if fill and bounds then +return bounds-padding +end +end +function rtk.Widget:_get_content_size(boxw,boxh,fillw,fillh,clampw,clamph,scale)local calc=self.calc +local tp,rp,bp,lp=self:_get_padding_and_border()local w=_get_content_dimension(self.w,boxw,lp+rp,fillw,clampw,calc.scalability,scale)local h=_get_content_dimension(self.h,boxh,tp+bp,fillh,clamph,calc.scalability,scale)if w and(calc.minw or calc.maxw)then +w=rtk.clamp(w,calc.minw,calc.maxw)end +if h and(calc.minh or calc.maxh)then +h=rtk.clamp(h,calc.minh,calc.maxh)end +return w,h,tp,rp,bp,lp +end +function rtk.Widget:_reflow(boxx,boxy,boxw,boxh,fillw,fillh,clampw,clamph,uiscale,viewport,window)local calc=self.calc +calc.x,calc.y=self:_get_box_pos(boxx,boxy)local w,h,tp,rp,bp,lp=self:_get_content_size(boxw,boxh,fillw,fillh,clampw,clamph)calc.w=w or(fillw and(boxw-lp-rp)or 0)calc.h=h or(fillh and(boxh-tp-bp)or 0)return fillw,fillh +end +function rtk.Widget:_realize_geometry()self.realized=true +end +function rtk.Widget:_slow_get_window()if self.window then +return self.window +end +local w=self.parent +while w do +if w.window then +return w.window +end +w=w.parent +end +end +function rtk.Widget:_is_mouse_over(clparentx,clparenty,event)local calc=self.calc +local x,y=calc.x+clparentx,calc.y+clparenty +return self.window and self.window.in_window and +rtk.point_in_box(event.x,event.y,x,y,calc.w,calc.h)end +function rtk.Widget:_draw(offx,offy,alpha,event,clipw,cliph,cltargetx,cltargety,parentx,parenty)self.offx=offx +self.offy=offy +self.clientx=cltargetx+offx+self.calc.x +self.clienty=cltargety+offy+self.calc.y +self.drawn=true +end +function rtk.Widget:_draw_bg(offx,offy,alpha,event)local calc=self.calc +if calc.bg and not calc.ghost then +self:setcolor(calc.bg,alpha)gfx.rect(calc.x+offx,calc.y+offy,calc.w,calc.h,1)end +end +function rtk.Widget:_draw_tooltip(clientx,clienty,clientw,clienth)local font=rtk.Font(table.unpack(rtk.theme.tooltip_font))local segments,w,h=font:layout(self.calc.tooltip,clientw-10,clienth-10,true)rtk.color.set(rtk.theme.tooltip_bg)local x=rtk.clamp(clientx,0,clientw-w-10)local y=rtk.clamp(clienty+16,0,clienth-h-10-self.calc.h)gfx.rect(x,y,w+10,h+10,1)rtk.color.set(rtk.theme.tooltip_text)gfx.rect(x,y,w+10,h+10,0)font:draw(segments,x+5,y+5,w,h)end +function rtk.Widget:_unpack_border(border,alpha)local color,thickness=table.unpack(border)if color then +self:setcolor(color or rtk.theme.button,alpha*self.calc.alpha)end +return thickness or 1 +end +function rtk.Widget:_draw_borders(offx,offy,alpha,all)if self.ghost then +return +end +local calc=self.calc +if not all and calc.border_uniform and not calc.tborder then +return +end +local x,y,w,h=calc.x+offx,calc.y+offy,calc.w,calc.h +local tb,rb,bb,lb +local all=all or(calc.border_uniform and calc.tborder)if all then +local thickness=self:_unpack_border(all,alpha)if thickness==1 then +gfx.rect(x,y,w,h,0)return +else +tb,rb,bb,lb=all,all,all,all +end +else +tb,rb,bb,lb=calc.tborder,calc.rborder,calc.bborder,calc.lborder +end +if tb then +local thickness=self:_unpack_border(tb,alpha)gfx.rect(x,y,w,thickness,1)end +if rb and w>0 then +local thickness=self:_unpack_border(rb,alpha)gfx.rect(x+w-thickness,y,thickness,h,1)end +if bb and h>0 then +local thickness=self:_unpack_border(bb,alpha)gfx.rect(x,y+h-thickness,w,thickness,1)end +if lb then +local thickness=self:_unpack_border(lb,alpha)gfx.rect(x,y,thickness,h,1)end +end +function rtk.Widget:_get_touch_activate_delay(event)if not rtk.touchscroll then +return self.touch_activate_delay or 0 +else +if not self.viewport or not self.viewport:scrollable()then +return 0 +end +return(not self:focused()and event.button==rtk.mouse.BUTTON_LEFT)and +self.touch_activate_delay or rtk.touch_activate_delay +end +end +function rtk.Widget:_should_handle_event(listen)if not listen and rtk._modal and rtk._modal[self.id]~=nil then +return true +else +return listen +end +end +function rtk.Widget:_handle_event(clparentx,clparenty,event,clipped,listen)local calc=self.calc +if not listen and rtk._modal and rtk._modal[self.id]==nil then +return false +end +local dnd=rtk.dnd +if not clipped and self:_is_mouse_over(clparentx,clparenty,event)then +event:set_widget_mouseover(self,clparentx,clparenty)if event.type==rtk.Event.MOUSEMOVE and not calc.disabled then +if dnd.dragging==self then +if calc.cursor then +self.window:request_mouse_cursor(calc.cursor)end +self:_handle_dragmousemove(event,dnd.arg)elseif self.hovering==false then +if event.buttons==0 or self:focused()then +if not event.handled and not self.mouseover and self:_handle_mouseenter(event)then +self.hovering=true +self:_handle_mousemove(event)self:queue_draw()elseif rtk.debug then +self:queue_draw()end +else +if dnd.arg and not event.simulated and rtk.dnd.droppable then +if dnd.dropping==self or self:_handle_dropfocus(event,dnd.dragging,dnd.arg)then +if dnd.dropping then +if dnd.dropping~=self then +dnd.dropping:_handle_dropblur(event,dnd.dragging,dnd.arg)elseif not event.simulated then +dnd.dropping:_handle_dropmousemove(event,dnd.dragging,dnd.arg)end +end +event:set_handled(self)self:queue_draw()dnd.dropping=self +end +end +end +if not self.mouseover and(not event.handled or event.handled==self)and event.buttons==0 then +self.mouseover=true +self:queue_draw()end +if self.mouseover and calc.cursor then +self.window:request_mouse_cursor(calc.cursor)end +else +if event.handled then +self:_handle_mouseleave(event)self.hovering=false +self.mouseover=false +self:queue_draw()else +self.mouseover=true +self.window:request_mouse_cursor(calc.cursor)self:_handle_mousemove(event)event:set_handled(self)end +end +elseif event.type==rtk.Event.MOUSEDOWN and not calc.disabled then +local duration=event:get_button_duration()if duration==0 then +event:set_widget_pressed(self)end +if not event.handled then +local state=event:get_button_state(self)or 0 +local threshold=self:_get_touch_activate_delay(event)if duration>=threshold and state==0 and event:is_widget_pressed(self)then +event:set_button_state(self,1)if self:_handle_mousedown(event)then +self:_accept_mousedown(event,duration,state)end +elseif state&8==0 then +if duration>=rtk.long_press_delay then +if self:_handle_longpress(event)then +self:queue_draw()event:set_button_state(self,state|8|16)else +event:set_button_state(self,state|8)end +end +end +if self:focused()then +event:set_handled(self)end +end +if self.mouseover and calc.cursor then +self.window:request_mouse_cursor(calc.cursor)end +elseif event.type==rtk.Event.MOUSEUP and not calc.disabled then +if not event.handled then +if not dnd.dragging then +self:_deferred_mousedown(event)end +if self:_handle_mouseup(event)then +event:set_handled(self)self:queue_draw()end +local state=event:get_button_state(self)or 0 +if state>0 or event:is_widget_pressed(self)then +if state&16==0 and not dnd.dragging then +if self:_handle_click(event)then +event:set_handled(self)self:queue_draw()end +if state&4~=0 then +if self:_handle_doubleclick(event)then +event:set_handled(self)self:queue_draw()end +self._last_mousedown_time=0 +end +end +end +if self.mouseover and calc.cursor then +self.window:request_mouse_cursor(calc.cursor)end +end +if dnd.dropping==self then +self:_handle_dropblur(event,dnd.dragging,dnd.arg)if self:_handle_drop(event,dnd.dragging,dnd.arg)then +event:set_handled(self)self:queue_draw()end +end +self:queue_draw()elseif event.type==rtk.Event.MOUSEWHEEL and not calc.disabled then +if not event.handled and self:_handle_mousewheel(event)then +event:set_handled(self)self:queue_draw()end +elseif event.type==rtk.Event.DROPFILE and not calc.disabled then +if not event.handled and self:_handle_dropfile(event)then +event:set_handled(self)self:queue_draw()end +end +elseif event.type==rtk.Event.MOUSEMOVE then +self.mouseover=false +if dnd.dragging==self then +self.window:request_mouse_cursor(calc.cursor)self:_handle_dragmousemove(event,dnd.arg)end +if self.hovering==true then +if dnd.dragging~=self then +self:_handle_mouseleave(event)self:queue_draw()self.hovering=false +end +elseif event.buttons~=0 and dnd.dropping then +if dnd.dropping==self then +self:_handle_dropblur(event,dnd.dragging,dnd.arg)dnd.dropping=nil +end +self:queue_draw()end +end +if rtk.touchscroll and event.type==rtk.Event.MOUSEUP and self:focused()then +if event:get_button_state('mousedown-handled') == self then +event:set_handled(self)self:queue_draw()end +end +if event.type==rtk.Event.KEY and not event.handled and self:focused()then +if self:_handle_keypress(event)then +event:set_handled(self)self:queue_draw()end +if self.mouseover and calc.cursor then +self.window:request_mouse_cursor(calc.cursor)end +end +return true +end +function rtk.Widget:_deferred_mousedown(event,x,y)local mousedown_handled=event:get_button_state('mousedown-handled')if not mousedown_handled and event:is_widget_pressed(self)and not event:get_button_state(self)then +local downevent=event:clone{type=rtk.Event.MOUSEDOWN,simulated=true,x=x or event.x,y=y or event.y}if self:_handle_mousedown(downevent)then +self:_accept_mousedown(event)end +end +end +function rtk.Widget:_accept_mousedown(event,duration,state)event:set_button_state('mousedown-handled', self)event:set_handled(self)if not event.simulated and event.time-self._last_mousedown_time<=rtk.double_click_delay then +event:set_button_state(self,(state or 0)|4)self._last_mousedown_time=0 +else +self._last_mousedown_time=event.time +end +self:queue_draw()end +function rtk.Widget:_unrealize()self.realized=false +end +function rtk.Widget:_release_modal(event)end +function rtk.Widget:onattr(attr,value,oldval,trigger,sync)return true end +function rtk.Widget:_handle_attr(attr,value,oldval,trigger,reflow,sync)local ok=self:onattr(attr,value,oldval,trigger,sync)if ok~=false then +local redraw +if reflow==rtk.Widget.REFLOW_DEFAULT then +local meta=self.class.attributes.get(attr)reflow=meta.reflow or rtk.Widget.REFLOW_PARTIAL +redraw=meta.redraw +end +if reflow~=rtk.Widget.REFLOW_NONE then +self:queue_reflow(reflow)elseif redraw~=false then +self:queue_draw()end +if attr=='visible' then +if not value then +self:_unrealize()end +self.realized=false +self.drawn=false +elseif attr=='ref' then +assert(not oldval, 'ref cannot be changed')self.refs[self.ref]=self +rtk._refs[self.ref]=self +if self.parent then +self.parent:_sync_child_refs(self, 'add')end +end +end +return ok +end +function rtk.Widget:ondrawpre(offx,offy,alpha,event)end +function rtk.Widget:_handle_drawpre(offx,offy,alpha,event)return self:ondrawpre(offx,offy,alpha,event)end +function rtk.Widget:ondraw(offx,offy,alpha,event)end +function rtk.Widget:_handle_draw(offx,offy,alpha,event)return self:ondraw(offx,offy,alpha,event)end +function rtk.Widget:onmousedown(event)end +function rtk.Widget:_handle_mousedown(event)local ok=self:onmousedown(event)if ok~=false then +if self.calc.autofocus then +self:focus(event)return ok or self:focused()else +return ok or false +end +end +return ok +end +function rtk.Widget:onmouseup(event)end +function rtk.Widget:_handle_mouseup(event)return self:onmouseup(event)end +function rtk.Widget:onmousewheel(event)end +function rtk.Widget:_handle_mousewheel(event)return self:onmousewheel(event)end +function rtk.Widget:onclick(event)end +function rtk.Widget:_handle_click(event)return self:onclick(event)end +function rtk.Widget:ondoubleclick(event)end +function rtk.Widget:_handle_doubleclick(event)return self:ondoubleclick(event)end +function rtk.Widget:onlongpress(event)end +function rtk.Widget:_handle_longpress(event)return self:onlongpress(event)end +function rtk.Widget:onmouseenter(event)end +function rtk.Widget:_handle_mouseenter(event)local ok=self:onmouseenter(event)if ok~=false then +return self.calc.autofocus or ok +end +return ok +end +function rtk.Widget:onmouseleave(event)end +function rtk.Widget:_handle_mouseleave(event)return self:onmouseleave(event)end +function rtk.Widget:onmousemove(event)end +rtk.Widget.onmousemove=nil +function rtk.Widget:_handle_mousemove(event)if self.onmousemove then +return self:onmousemove(event)end +end +function rtk.Widget:onkeypress(event)end +function rtk.Widget:_handle_keypress(event)return self:onkeypress(event)end +function rtk.Widget:onfocus(event)return true +end +function rtk.Widget:_handle_focus(event)return self:onfocus(event)end +function rtk.Widget:onblur(event,other)return true +end +function rtk.Widget:_handle_blur(event,other)return self:onblur(event,other)end +function rtk.Widget:ondragstart(event,x,y,t)end +function rtk.Widget:_handle_dragstart(event,x,y,t)local draggable,droppable=self:ondragstart(event,x,y,t)if draggable==nil then +return false,false +end +return draggable,droppable +end +function rtk.Widget:ondragend(event,dragarg)end +function rtk.Widget:_handle_dragend(event,dragarg)self._last_mousedown_time=0 +return self:ondragend(event,dragarg)end +function rtk.Widget:ondragmousemove(event,dragarg)end +function rtk.Widget:_handle_dragmousemove(event,dragarg)return self:ondragmousemove(event,dragarg)end +function rtk.Widget:ondropfocus(event,source,dragarg)return false +end +function rtk.Widget:_handle_dropfocus(event,source,dragarg)return self:ondropfocus(event,source,dragarg)end +function rtk.Widget:ondropmousemove(event,source,dragarg)end +function rtk.Widget:_handle_dropmousemove(event,source,dragarg)return self:ondropmousemove(event,source,dragarg)end +function rtk.Widget:ondropblur(event,source,dragarg)end +function rtk.Widget:_handle_dropblur(event,source,dragarg)return self:ondropblur(event,source,dragarg)end +function rtk.Widget:ondrop(event,source,dragarg)return false +end +function rtk.Widget:_handle_drop(event,source,dragarg)return self:ondrop(event,source,dragarg)end +function rtk.Widget:onreflow()end +function rtk.Widget:_handle_reflow()return self:onreflow()end +function rtk.Widget:ondropfile(event)end +function rtk.Widget:_handle_dropfile(event)return self:ondropfile(event)end +end)() + +__mod_rtk_viewport=(function() +local rtk=__mod_rtk_core +rtk.Viewport=rtk.class('rtk.Viewport', rtk.Widget)rtk.Viewport.static.SCROLLBAR_NEVER=0 +rtk.Viewport.static.SCROLLBAR_HOVER=1 +rtk.Viewport.static.SCROLLBAR_ALWAYS=2 +rtk.Viewport.register{[1]=rtk.Attribute{alias='child'},child=rtk.Attribute{reflow=rtk.Widget.REFLOW_FULL},scroll_left=rtk.Attribute{default=0,reflow=rtk.Widget.REFLOW_NONE,calculate=function(self,attr,value,target)return math.round(value)end,},scroll_top=rtk.Reference('scroll_left'),smoothscroll=rtk.Attribute{reflow=rtk.Widget.REFLOW_NONE},scrollbar_size=15,vscrollbar=rtk.Attribute{default=rtk.Viewport.SCROLLBAR_HOVER,calculate={never=rtk.Viewport.SCROLLBAR_NEVER,always=rtk.Viewport.SCROLLBAR_ALWAYS,hover=rtk.Viewport.SCROLLBAR_HOVER,},},vscrollbar_offset=rtk.Attribute{default=0,reflow=rtk.Widget.REFLOW_NONE,},vscrollbar_gutter=25,hscrollbar=rtk.Attribute{default=rtk.Viewport.SCROLLBAR_NEVER,calculate=rtk.Reference('vscrollbar'),},hscrollbar_offset=0,hscrollbar_gutter=25,flexw=false,flexh=true,shadow=nil,elevation=20,show_scrollbar_on_drag=false,touch_activate_delay=0,}function rtk.Viewport:initialize(attrs,...)rtk.Widget.initialize(self,attrs,self.class.attributes.defaults,...)self:_handle_attr('child', self.calc.child, nil, true)self:_handle_attr('bg', self.calc.bg)self._backingstore=nil +self._needs_clamping=false +self._last_draw_scroll_left=nil +self._last_draw_scroll_top=nil +self._vscrollx=0 +self._vscrolly=0 +self._vscrollh=0 +self._vscrolla={current=self.calc.vscrollbar==rtk.Viewport.SCROLLBAR_ALWAYS and 0.1 or 0,target=0,delta=0.05 +}self._vscroll_in_gutter=false +end +function rtk.Viewport:_handle_attr(attr,value,oldval,trigger,reflow,sync)local ok=rtk.Widget._handle_attr(self,attr,value,oldval,trigger,reflow,sync)if ok==false then +return ok +end +if attr=='child' then +if oldval then +oldval:_unrealize()oldval.viewport=nil +oldval.parent=nil +oldval.window=nil +self:_sync_child_refs(oldval, 'remove')end +if value then +value.viewport=self +value.parent=self +value.window=self.window +self:_sync_child_refs(value, 'add')end +elseif attr=='bg' then +value=value or rtk.theme.bg +local luma=rtk.color.luma(value)local offset=math.max(0,1-(1.5-3*luma)^2)self._scrollbar_alpha_proximity=0.19*(1+offset^0.2)self._scrollbar_alpha_hover=0.44*(1+offset^0.4)self._scrollbar_color=luma < 0.5 and '#ffffff' or '#000000'elseif attr=='shadow' then +self._shadow=nil +elseif attr == 'scroll_top' or attr == 'scroll_left' then +self._needs_clamping=true +end +return true +end +function rtk.Viewport:_sync_child_refs(child,action)return rtk.Container._sync_child_refs(self,child,action)end +function rtk.Viewport:_reflow(boxx,boxy,boxw,boxh,fillw,fillh,clampw,clamph,uiscale,viewport,window)local calc=self.calc +calc.x,calc.y=self:_get_box_pos(boxx,boxy)local w,h,tp,rp,bp,lp=self:_get_content_size(boxw,boxh,fillw,fillh,clampw,clamph)local hpadding=lp+rp +local vpadding=tp+bp +local inner_maxw=w or(boxw-hpadding)local inner_maxh=h or(boxh-vpadding)local scrollw,scrollh=0,0 +if calc.vscrollbar==rtk.Viewport.SCROLLBAR_ALWAYS then +scrollw=calc.scrollbar_size*rtk.scale.value +inner_maxw=inner_maxw-scrollw +end +if calc.hscrollbar==rtk.Viewport.SCROLLBAR_ALWAYS then +scrollh=calc.scrollbar_size*rtk.scale.value +inner_maxh=inner_maxh-scrollh +end +local child=calc.child +local innerw,innerh +local hmargin,vmargin +local ccalc +if child and child.visible==true then +ccalc=child.calc +hmargin=ccalc.lmargin+ccalc.rmargin +vmargin=ccalc.tmargin+ccalc.bmargin +inner_maxw=inner_maxw-hmargin +inner_maxh=inner_maxh-vmargin +local wx,wy,ww,wh=child:reflow(0,0,inner_maxw,inner_maxh,false,false,not calc.flexw,not calc.flexh,uiscale,self,window +)if calc.halign==rtk.Widget.CENTER then +wx=wx+math.max(0,inner_maxw-ccalc.w)/2 +elseif calc.halign==rtk.Widget.RIGHT then +wx=wx+math.max(0,(inner_maxw-ccalc.w)-rp)end +if calc.valign==rtk.Widget.CENTER then +wy=wy+math.max(0,inner_maxh-ccalc.h)/2 +elseif calc.valign==rtk.Widget.BOTTOM then +wy=wy+math.max(0,(inner_maxh-ccalc.h)-bp)end +ccalc.x=wx +ccalc.y=wy +child:_realize_geometry()innerw=math.ceil(rtk.clamp(ww+wx,fillw and inner_maxw,inner_maxw))innerh=math.ceil(rtk.clamp(wh+wy,fillh and inner_maxh,inner_maxh))else +innerw,innerh=inner_maxw,inner_maxh +hmargin,vmargin=0,0 +end +calc.w=(w or(innerw+scrollw+hmargin))+hpadding +calc.h=(h or(innerh+scrollh+vmargin))+vpadding +if not self._backingstore then +self._backingstore=rtk.Image(innerw,innerh)else +self._backingstore:resize(innerw,innerh,false)end +self._vscrollh=0 +self._needs_clamping=true +if ccalc then +self._scroll_clamp_left=math.max(0,ccalc.w-calc.w+lp+rp+ccalc.lmargin+ccalc.rmargin)self._scroll_clamp_top=math.max(0,ccalc.h-calc.h+tp+bp+ccalc.tmargin+ccalc.bmargin)end +end +function rtk.Viewport:_realize_geometry()local calc=self.calc +local tp,rp,bp,lp=self:_get_padding_and_border()if self.child then +local innerh=self._backingstore.h +local ch=self.child.calc.h +if calc.vscrollbar~=rtk.Viewport.SCROLLBAR_NEVER and ch>innerh then +self._vscrollx=calc.x+calc.w-calc.scrollbar_size*rtk.scale.value-calc.vscrollbar_offset +self._vscrolly=calc.y+calc.h*calc.scroll_top/ch+tp +self._vscrollh=calc.h*innerh/ch +end +end +if self.shadow then +if not self._shadow then +self._shadow=rtk.Shadow(calc.shadow)end +self._shadow:set_rectangle(calc.w,calc.h,calc.elevation)end +self._pre={tp=tp,rp=rp,bp=bp,lp=lp}end +function rtk.Viewport:_unrealize()self._backingstore=nil +if self.child then +self.child:_unrealize()end +end +function rtk.Viewport:_draw(offx,offy,alpha,event,clipw,cliph,cltargetx,cltargety,parentx,parenty)rtk.Widget._draw(self,offx,offy,alpha,event,clipw,cliph,cltargetx,cltargety,parentx,parenty)local calc=self.calc +local pre=self._pre +self.cltargetx=cltargetx +self.cltargety=cltargety +local x=calc.x+offx+pre.lp +local y=calc.y+offy+pre.tp +local lastleft,lasttop +local scrolled=calc.scroll_left~=self._last_draw_scroll_left or +calc.scroll_top~=self._last_draw_scroll_top +if scrolled then +lastleft,lasttop=self._last_draw_scroll_left or 0,self._last_draw_scroll_top or 0 +if self:onscrollpre(lastleft,lasttop,event)==false then +calc.scroll_left=lastleft or 0 +calc.scroll_top=lasttop +scrolled=false +else +self._last_draw_scroll_left=calc.scroll_left +self._last_draw_scroll_top=calc.scroll_top +end +end +if y+calc.h<0 or y>cliph or calc.ghost then +return false +end +self:_handle_drawpre(offx,offy,alpha,event)self:_draw_bg(offx,offy,1.0,event)local child=calc.child +if child and child.realized then +self:_clamp()x=x+child.calc.lmargin +y=y+child.calc.tmargin +self._backingstore:blit{src=gfx.dest,sx=x,sy=y,mode=rtk.Image.FAST_BLIT}self._backingstore:pushdest()child:_draw(-calc.scroll_left,-calc.scroll_top,1.0,event,calc.w,calc.h,cltargetx+x,cltargety+y,0,0 +)child:_draw_debug_box(-calc.scroll_left,-calc.scroll_top,event)self._backingstore:popdest()self._backingstore:blit{dx=x,dy=y,alpha=alpha*calc.alpha}self:_draw_scrollbars(offx,offy,cltargetx,cltargety,alpha*calc.alpha,event)end +if calc.shadow then +self._shadow:draw(calc.x+offx,calc.y+offy,alpha*calc.alpha)end +self:_draw_borders(offx,offy,alpha)if scrolled then +self:onscroll(lastleft,lasttop,event)end +self:_handle_draw(offx,offy,alpha,event)end +function rtk.Viewport:_draw_scrollbars(offx,offy,cltargetx,cltargety,alpha,event)local calc=self.calc +local animate=self._vscrolla.current~=self._vscrolla.target +if calc.vscrollbar==rtk.Viewport.SCROLLBAR_ALWAYS or +(calc.vscrollbar==rtk.Viewport.SCROLLBAR_HOVER and self._vscrollh>0 and +((not rtk.dnd.dragging and self._vscroll_in_gutter)or animate or self._vscrolla.target>0))then +local scrx=offx+self._vscrollx +local scry=offy+calc.y+calc.h*calc.scroll_top/self.child.calc.h +local should_handle_hovering=rtk.point_in_box(event.x,event.y,scrx+cltargetx,scry+cltargety,calc.scrollbar_size*rtk.scale.value,self._vscrollh +)if(should_handle_hovering and self._vscroll_in_gutter)or rtk.dnd.dragging==self then +self._vscrolla.target=self._scrollbar_alpha_hover +self._vscrolla.delta=0.1 +elseif self._vscroll_in_gutter or calc.vscrollbar==rtk.Viewport.SCROLLBAR_ALWAYS then +self._vscrolla.target=self._scrollbar_alpha_proximity +self._vscrolla.delta=0.1 +end +if animate then +local newval +if self._vscrolla.currenty+calc.h then +self:scrollby(0,math.max(5,math.abs(y+calc.h-event.y)),false)end +if dragging.show_scrollbar_on_drag then +self._vscrolla.target=self._scrollbar_alpha_proximity +self._vscrolla.delta=0.03 +end +elseif listen and not dragging and not event.handled and hovering then +if calc.vscrollbar~=rtk.Viewport.SCROLLBAR_NEVER and self._vscrollh>0 then +local gutterx=self._vscrollx+clparentx-calc.vscrollbar_gutter +local guttery=calc.y+clparenty +if rtk.point_in_box(event.x,event.y,gutterx,guttery,calc.vscrollbar_gutter+calc.scrollbar_size*rtk.scale.value,calc.h)then +vscroll_in_gutter=true +if event.x>=self._vscrollx+clparentx then +event:set_handled(self)end +self:queue_draw()end +end +end +if vscroll_in_gutter~=self._vscroll_in_gutter or self._vscrolla.current>0 then +self._vscroll_in_gutter=vscroll_in_gutter +if calc.vscrollbar==rtk.Viewport.SCROLLBAR_HOVER then +if not vscroll_in_gutter and not is_child_dragging then +self._vscrolla.target=0 +self._vscrolla.delta=0.02 +end +self:queue_draw()end +end +elseif listen and not event.handled and event.type==rtk.Event.MOUSEDOWN then +if not self:cancel_animation('scroll_top') then +self:_reset_touch_scroll()end +if self._vscroll_in_gutter and event.x>=self._vscrollx+clparentx then +local scrolly=self:_get_vscrollbar_client_pos()if event.yscrolly+self._vscrollh then +self:_handle_scrollbar(event,nil,self._vscrollh/2,true)end +event:set_handled(self)end +end +if(not event.handled or event.type==rtk.Event.MOUSEMOVE)and +not(event.type==rtk.Event.MOUSEMOVE and self.window:_is_touch_scrolling(self))and +self.child and self.child.visible and self.child.realized then +self:_clamp()self.child:_handle_event(x-calc.scroll_left+pre.lp+child.calc.lmargin,y-calc.scroll_top+pre.tp+child.calc.tmargin,event,clipped or not hovering,listen +)end +if listen and hovering and not event.handled and event.type==rtk.Event.MOUSEWHEEL then +if self.child and self._vscrollh>0 and event.wheel~=0 then +local distance=event.wheel*math.min(calc.h/2,120)self:scrollby(0,distance)event:set_handled(self)end +end +rtk.Widget._handle_event(self,clparentx,clparenty,event,clipped,listen)end +function rtk.Viewport:_get_vscrollbar_client_pos()local calc=self.calc +return self.clienty+calc.h*calc.scroll_top/self.child.calc.h +end +function rtk.Viewport:_handle_scrollbar(event,hoffset,voffset,gutteronly,natural)local calc=self.calc +local pre=self._pre +if voffset~=nil then +self:cancel_animation('scroll_top')if gutteronly then +local ssy=self:_get_vscrollbar_client_pos()if event.y>=ssy and event.y<=ssy+self._vscrollh then +return false +end +end +local innerh=calc.h-pre.tp-pre.bp +local target +if natural then +target=calc.scroll_top+(voffset-event.y)else +local pct=rtk.clamp(event.y-self.clienty-voffset,0,innerh)/innerh +target=pct*(self.child.calc.h)end +self:scrollto(calc.scroll_left,target,false)end +end +function rtk.Viewport:_handle_dragstart(event,x,y,t)local draggable,droppable=self:ondragstart(self,event,x,y,t)if draggable~=nil then +return draggable,droppable +end +if math.abs(y-event.y)>0 then +if self._vscroll_in_gutter and event.x>=self._vscrollx+self.offx+self.cltargetx then +return {true,y-self:_get_vscrollbar_client_pos(),nil,false},false +elseif rtk.touchscroll and event.buttons&rtk.mouse.BUTTON_LEFT~=0 and self._vscrollh>0 then +self.window:_set_touch_scrolling(self,true)return {true,y,{{x,y,t}},true},false +end +end +return false,false +end +function rtk.Viewport:_handle_dragmousemove(event,arg)local ok=rtk.Widget._handle_dragmousemove(self,event)if ok==false or event.simulated then +return ok +end +local vscrollbar,lasty,samples,natural=table.unpack(arg)if vscrollbar then +self:_handle_scrollbar(event,nil,lasty,false,natural)if natural then +arg[2]=event.y +samples[#samples+1]={event.x,event.y,event.time}end +self.window:request_mouse_cursor(rtk.mouse.cursors.POINTER,true)end +return true +end +function rtk.Viewport:_reset_touch_scroll()if self.window then +self.window:_set_touch_scrolling(self,false)end +end +function rtk.Viewport:_handle_dragend(event,arg)local ok=rtk.Widget._handle_dragend(self,event)if ok==false then +return ok +end +local vscrollbar,lasty,samples,natural=table.unpack(arg)if natural then +local now=event.time +local x1,y1,t1=event.x,event.y,event.time +for i=#samples,1,-1 do +local x,y,t=table.unpack(samples[i])if now-t>0.2 then +break +end +x1,y1,t1=x,y,t +end +local v=0 +if t1~=event.time then +v=(event.y-y1)-(event.time-t1)end +local distance=v*rtk.scale.value +local x,y=self:_get_clamped_scroll(self.calc.scroll_left,self.calc.scroll_top-distance)local duration=1 +self:animate{attr='scroll_top', dst=y, duration=duration, easing='out-cubic'}:done(function()self:_reset_touch_scroll()end):cancelled(function()self:_reset_touch_scroll()end)end +self:queue_draw()event:set_handled(self)return true +end +function rtk.Viewport:_scrollto(x,y,smooth,animx,animy)local calc=self.calc +if not smooth or not self.realized then +x=x or self.scroll_left +y=y or self.scroll_top +if x==calc.scroll_left and y==calc.scroll_top then +return +end +self._needs_clamping=true +calc.scroll_left=x +calc.scroll_top=y +self.scroll_left=calc.scroll_left +self.scroll_top=calc.scroll_top +self:queue_draw()else +x,y=self:_get_clamped_scroll(x or calc.scroll_left,y or calc.scroll_top)animx=animx or self:get_animation('scroll_left')animy=animy or self:get_animation('scroll_top')if calc.scroll_left~=x and(not animx or animx.dst~=x)then +self:animate{attr='scroll_left', dst=x, duration=0.15}end +if calc.scroll_top~=y and(not animy or animy.dst~=y)then +self:animate{attr='scroll_top', dst=y, duration=0.2, easing='out-sine'}end +end +end +function rtk.Viewport:_get_smoothscroll(override)if override~=nil then +return override +end +local calc=self.calc +if calc.smoothscroll~=nil then +return calc.smoothscroll +end +return rtk.smoothscroll +end +function rtk.Viewport:scrollto(x,y,smooth)self:_scrollto(x,y,self:_get_smoothscroll(smooth))end +function rtk.Viewport:scrollby(offx,offy,smooth)local calc=self.calc +local x,y,animx,animy +smooth=self:_get_smoothscroll(smooth)if smooth then +animx=self:get_animation('scroll_left')animy=self:get_animation('scroll_top')x=(animx and animx.dst or calc.scroll_left)+(offx or 0)y=(animy and animy.dst or calc.scroll_top)+(offy or 0)else +x=calc.scroll_left+(offx or 0)y=calc.scroll_top+(offy or 0)end +self:_scrollto(x,y,smooth,animx,animy)end +function rtk.Viewport:scrollable()if not self.child then +return false +end +local vcalc=self.calc +local ccalc=self.child.calc +return ccalc.w>vcalc.w or ccalc.h>vcalc.h +end +function rtk.Viewport:_get_clamped_scroll(left,top)return rtk.clamp(left,0,self._scroll_clamp_left),rtk.clamp(top,0,self._scroll_clamp_top)end +function rtk.Viewport:_clamp()if self._needs_clamping then +local calc=self.calc +calc.scroll_left,calc.scroll_top=self:_get_clamped_scroll(self.scroll_left,self.scroll_top)self.scroll_left,self.scroll_top=calc.scroll_left,calc.scroll_top +self._needs_clamping=false +end +end +function rtk.Viewport:onscrollpre(last_left,last_top,event)end +function rtk.Viewport:onscroll(last_left,last_top,event)end +end)() + +__mod_rtk_popup=(function() +local rtk=__mod_rtk_core +rtk.Popup=rtk.class('rtk.Popup', rtk.Viewport)rtk.Popup.register{anchor=rtk.Attribute{reflow=rtk.Widget.REFLOW_FULL},margin=rtk.Attribute{default=20,reflow=rtk.Widget.REFLOW_FULL,},width_from_anchor=rtk.Attribute{default=true,reflow=rtk.Widget.REFLOW_FULL,},overlay=rtk.Reference('bg'),autoclose=true,opened=false,bg=rtk.Attribute{default=function(self,attr)return rtk.theme.popup_bg or {rtk.color.mod(rtk.theme.bg,1,1,rtk.theme.popup_bg_brightness,0.96)}end,},border=rtk.Attribute{default=function(self,attr)return rtk.theme.popup_border +end,},shadow=rtk.Attribute{default=function()return rtk.theme.popup_shadow +end,},visible=false,elevation=35,padding=10,z=1000,}function rtk.Popup:initialize(attrs,...)rtk.Viewport.initialize(self,attrs,self.class.attributes.defaults,...)end +function rtk.Popup:_handle_event(clparentx,clparenty,event,clipped,listen)rtk.Viewport._handle_event(self,clparentx,clparenty,event,clipped,listen)if event.type==rtk._touch_activate_event and self.mouseover then +event:set_handled(self)end +end +function rtk.Popup:_reflow(boxx,boxy,boxw,boxh,fillw,fillh,clampw,clamph,rescale,viewport,window)local calc=self.calc +local anchor=calc.anchor +if anchor then +y=anchor.clienty +if y0 then +for i=1,#attrs do +local w=attrs[i] +self:add(w)end +end +end +function rtk.Container:_handle_mouseenter(event)local ret=self:onmouseenter(event)if ret~=false then +if self.bg or self.autofocus then +return true +end +end +return ret +end +function rtk.Container:_handle_mousemove(event)local ret=rtk.Widget._handle_mousemove(self,event)if ret~=false and self.hovering then +event:set_handled(self)return true +end +return ret +end +function rtk.Container:_draw_debug_box(offx,offy,event)if not rtk.Widget._draw_debug_box(self,offx,offy,event)then +return +end +gfx.set(1,1,1,1)for i=1,#self.children do +local widget,attrs=table.unpack(self.children[i])local cb=attrs._cellbox +if cb and widget.visible then +gfx.rect(offx+self.calc.x+cb[1],offy+self.calc.y+cb[2],cb[3],cb[4],0)end +end +end +function rtk.Container:_sync_child_refs(child,action)if child.refs and not child.refs.__empty then +if action=='add' then +local w=self +while w do +table.merge(w.refs,child.refs)w=w.parent +end +else +for k in pairs(child.refs)do +self.refs[k]=nil +end +end +end +end +function rtk.Container:_validate_child(child)assert(rtk.isa(child, rtk.Widget), 'object being added to container is not subclassed from rtk.Widget')end +function rtk.Container:_reparent_child(child)self:_validate_child(child)if child.parent and child.parent~=self then +child.parent:remove(child)end +child.parent=self +child.window=self.window +self:_sync_child_refs(child, 'add')end +function rtk.Container:_unparent_child(pos)local child=self.children[pos][1] +if child then +if child.visible then +child:_unrealize()end +child.parent=nil +child.window=nil +self:_sync_child_refs(child, 'remove')return child +end +end +function rtk.Container:add(widget,attrs)self:_reparent_child(widget)self.children[#self.children+1]={widget,self:_calc_cell_attrs(widget,attrs)}self._child_index_by_id=nil +self:queue_reflow(rtk.Widget.REFLOW_FULL)return widget +end +function rtk.Container:update(widget,attrs,merge)local n=self:get_child_index(widget)assert(n, 'Widget not found in container')attrs=self:_calc_cell_attrs(widget,attrs)if merge then +local cellattrs=self.children[n][2] +table.merge(cellattrs,attrs)else +self.children[n][2]=attrs +end +self:queue_reflow(rtk.Widget.REFLOW_FULL)end +function rtk.Container:insert(pos,widget,attrs)self:_reparent_child(widget)table.insert(self.children,pos,{widget,self:_calc_cell_attrs(widget,attrs)})self._child_index_by_id=nil +self:queue_reflow(rtk.Widget.REFLOW_FULL)end +function rtk.Container:replace(index,widget,attrs)if index<=0 or index>#self.children then +return +end +local prev=self:_unparent_child(index)self:_reparent_child(widget)self.children[index]={widget,self:_calc_cell_attrs(widget,attrs)}self._child_index_by_id=nil +self:queue_reflow(rtk.Widget.REFLOW_FULL)return prev +end +function rtk.Container:remove_index(index)if index<=0 or index>#self.children then +return +end +local child=self:_unparent_child(index)table.remove(self.children,index)self._child_index_by_id=nil +self:queue_reflow(rtk.Widget.REFLOW_FULL)return child +end +function rtk.Container:remove(widget)local n=self:get_child_index(widget)if n~=nil then +self:remove_index(n)return n +end +end +function rtk.Container:remove_all()for i=1,#self.children do +local widget=self.children[i][1] +if widget and widget.visible then +widget:_unrealize()end +end +self.children={}self._child_index_by_id=nil +self:queue_reflow(rtk.Widget.REFLOW_FULL)end +function rtk.Container:_calc_cell_attrs(widget,attrs)attrs=attrs or widget.cell +if not attrs then +return {}end +local keys=table.keys(attrs)local calculated={}for n=1,#keys do +local k=keys[n] +calculated[k]=self:_calc_attr(k, attrs[k], attrs, nil, 'cell', widget)end +return calculated +end +function rtk.Container:reorder(widget,targetidx)local srcidx=self:get_child_index(widget)if srcidx~=nil and srcidx~=targetidx and(targetidx<=srcidx or targetidx-1~=srcidx)then +local widgetattrs=table.remove(self.children,srcidx)local org=targetidx +if targetidx>srcidx then +targetidx=targetidx-1 +end +table.insert(self.children,rtk.clamp(targetidx,1,#self.children+1),widgetattrs)self._child_index_by_id=nil +self:queue_reflow(rtk.Widget.REFLOW_FULL)return true +else +return false +end +end +function rtk.Container:reorder_before(widget,target)local targetidx=self:get_child_index(target)return self:reorder(widget,targetidx)end +function rtk.Container:reorder_after(widget,target)local targetidx=self:get_child_index(target)return self:reorder(widget,targetidx+1)end +function rtk.Container:get_child(idx)if idx<0 then +idx=#self.children+idx+1 +end +local child=self.children[idx] +if child then +return child[1] +end +end +function rtk.Container:get_child_index(widget)if not self._child_index_by_id then +local cache={}for i=1,#self.children do +local widgetattrs=self.children[i] +if widgetattrs and widgetattrs[1].id then +cache[widgetattrs[1].id]=i +end +end +self._child_index_by_id=cache +end +return self._child_index_by_id[widget.id] +end +function rtk.Container:_handle_event(clparentx,clparenty,event,clipped,listen)local calc=self.calc +local x=calc.x+clparentx +local y=calc.y+clparenty +self.clientx,self.clienty=x,y +listen=self:_should_handle_event(listen)if y+calc.h<0 or y>self.window.calc.h or calc.ghost then +return false +end +local zs=self._z_indexes +for zidx=#zs,1,-1 do +local zchildren=self._reflowed_children[zs[zidx]] +local nzchildren=zchildren and #zchildren or 0 +for cidx=nzchildren,1,-1 do +local widget,attrs=table.unpack(zchildren[cidx])if widget and widget.realized and widget.parent then +if widget.calc.position&rtk.Widget.POSITION_FIXED~=0 and self.viewport then +local vcalc=self.viewport.calc +widget:_handle_event(x+vcalc.scroll_left,y+vcalc.scroll_top,event,clipped,listen)else +widget:_handle_event(x,y,event,clipped,listen)end +end +end +end +rtk.Widget._handle_event(self,clparentx,clparenty,event,clipped,listen)end +function rtk.Container:_add_reflowed_child(widgetattrs,z)local z_children=self._reflowed_children[z] +if z_children then +z_children[#z_children+1]=widgetattrs +else +self._reflowed_children[z]={widgetattrs}end +end +function rtk.Container:_determine_zorders()local zs={}for z in pairs(self._reflowed_children)do +zs[#zs+1]=z +end +table.sort(zs)self._z_indexes=zs +end +function rtk.Container:_get_cell_padding(widget,attrs)local calc=widget.calc +local scale=rtk.scale.value +return +((attrs.tpadding or 0)+(calc.tmargin or 0))*scale,((attrs.rpadding or 0)+(calc.rmargin or 0))*scale,((attrs.bpadding or 0)+(calc.bmargin or 0))*scale,((attrs.lpadding or 0)+(calc.lmargin or 0))*scale +end +function rtk.Container:_set_cell_box(attrs,x,y,w,h)attrs._cellbox={math.round(x),math.round(y),math.round(w),math.round(h)}end +function rtk.Container:_reflow(boxx,boxy,boxw,boxh,fillw,fillh,clampw,clamph,uiscale,viewport,window)local calc=self.calc +local x,y=self:_get_box_pos(boxx,boxy)local w,h,tp,rp,bp,lp=self:_get_content_size(boxw,boxh,fillw,fillh,clampw,clamph,nil)local inner_maxw=w or(boxw-lp-rp)local inner_maxh=h or(boxh-tp-bp)local innerw=w or 0 +local innerh=h or 0 +clampw=clampw or w~=nil or fillw +clamph=clamph or h~=nil or fillh +self._reflowed_children={}self._child_index_by_id={}for n,widgetattrs in ipairs(self.children)do +local widget,attrs=table.unpack(widgetattrs)local wcalc=widget.calc +attrs._cellbox=nil +self._child_index_by_id[widget.id]=n +if widget.visible==true then +local ctp,crp,cbp,clp=self:_get_cell_padding(widget,attrs)local wx,wy,ww,wh=widget:reflow(0,0,rtk.clamprel(inner_maxw-widget.x-clp-crp,attrs.minw or wcalc.minw,attrs.maxw or wcalc.maxw),rtk.clamprel(inner_maxh-widget.y-ctp-cbp,attrs.minh or wcalc.minh,attrs.maxh or wcalc.maxh),attrs.fillw,attrs.fillh,clampw or attrs.maxw~=nil,clamph or attrs.maxh~=nil,uiscale,viewport,window +)ww=math.max(ww,attrs.minw or wcalc.minw or 0)wh=math.max(wh,attrs.minh or wcalc.minh or 0)attrs._halign=attrs.halign or calc.halign +attrs._valign=attrs.valign or calc.valign +if not attrs._halign or attrs._halign==rtk.Widget.LEFT then +wx=lp+clp +elseif attrs._halign==rtk.Widget.CENTER then +wx=lp+clp+math.max(0,(math.min(innerw,inner_maxw)-ww-clp-crp)/2)else +wx=lp+math.max(0,math.min(innerw,inner_maxw)-ww-crp)end +if not attrs._valign or attrs._valign==rtk.Widget.TOP then +wy=tp+ctp +elseif attrs._valign==rtk.Widget.CENTER then +wy=tp+ctp+math.max(0,(math.min(innerh,inner_maxh)-wh-ctp-cbp)/2)else +wy=tp+math.max(0,math.min(innerh,inner_maxh)-wh-cbp)end +wcalc.x=wcalc.x+wx +widget.box[1]=wx +wcalc.y=wcalc.y+wy +widget.box[2]=wy +self:_set_cell_box(attrs,wcalc.x,wcalc.y,ww+clp+crp,wh+ctp+cbp)widget:_realize_geometry()innerw=math.max(innerw,wcalc.x+ww-lp+crp)innerh=math.max(innerh,wcalc.y+wh-tp+cbp)self:_add_reflowed_child(widgetattrs,attrs.z or wcalc.z or 0)else +widget.realized=false +end +end +self:_determine_zorders()calc.x=x +calc.y=y +calc.w=(w or innerw)+lp+rp +calc.h=(h or innerh)+tp+bp +end +function rtk.Container:_draw(offx,offy,alpha,event,clipw,cliph,cltargetx,cltargety,parentx,parenty)local calc=self.calc +rtk.Widget._draw(self,offx,offy,alpha,event,clipw,cliph,cltargetx,cltargety,parentx,parenty)local x,y=calc.x+offx,calc.y+offy +if y+calc.h<0 or y>cliph or calc.ghost then +return false +end +local wpx=parentx+calc.x +local wpy=parenty+calc.y +self:_handle_drawpre(offx,offy,alpha,event)self:_draw_bg(offx,offy,alpha,event)local child_alpha=alpha*self.alpha +for _,z in ipairs(self._z_indexes)do +for _,widgetattrs in ipairs(self._reflowed_children[z])do +local widget,attrs=table.unpack(widgetattrs)if attrs.bg and attrs._cellbox then +local cb=attrs._cellbox +self:setcolor(attrs.bg,child_alpha)gfx.rect(x+cb[1],y+cb[2],cb[3],cb[4],1)end +if widget and widget.realized then +local wx,wy=x,y +if widget.calc.position&rtk.Widget.POSITION_FIXED~=0 then +wx,wy=wpx,wpy +end +widget:_draw(wx,wy,child_alpha,event,clipw,cliph,cltargetx,cltargety,wpx,wpy)widget:_draw_debug_box(wx,wy,event)end +end +end +self:_draw_borders(offx,offy,alpha)self:_handle_draw(offx,offy,alpha,event)end +function rtk.Container:_unrealize()rtk.Widget._unrealize(self)for i=1,#self.children do +local widget=self.children[i][1] +if widget and widget.realized then +widget:_unrealize()end +end +end +end)() + +__mod_rtk_window=(function() +local rtk=__mod_rtk_core +local log=__mod_rtk_log +rtk.Window=rtk.class('rtk.Window', rtk.Container)rtk.Window.static.DOCK_BOTTOM=(function()return {0} end)()rtk.Window.static.DOCK_LEFT=(function()return {1} end)()rtk.Window.static.DOCK_TOP=(function()return {2} end)()rtk.Window.static.DOCK_RIGHT=(function()return {3} end)()rtk.Window.static.DOCK_FLOATING=(function()return {4} end)()function rtk.Window.static._make_icons()local w,h=12,12 +local sz=2 +local icon=rtk.Image(w,h)icon:pushdest()rtk.color.set(rtk.theme.dark and {1,1,1,1} or {0,0,0,1})for row=0,2 do +for col=0,2 do +local n=row*3+col +if n==2 or n>=4 then +gfx.rect(2*col*sz,2*row*sz,sz,sz,1)end +end +end +icon:popdest()rtk.Window.static._icon_resize_grip=icon +end +rtk.Window.register{x=rtk.Attribute{type='number',reflow=rtk.Widget.REFLOW_NONE,redraw=false,window_sync=true,},y=rtk.Attribute{type='number',reflow=rtk.Widget.REFLOW_NONE,redraw=false,window_sync=true,},w=rtk.Attribute{priority=true,type='number',window_sync=true,calculate=function(self,attr,value,target)return value and math.max(self.minw or 0,value)*rtk.scale.framebuffer +end,},h=rtk.Attribute{priority=true,window_sync=true,calculate=function(self,attr,value,target)return math.max(self.minh or 0,value or 0)*rtk.scale.framebuffer +end,},minw=rtk.Attribute{default=100,calculate=function(self,attr,value,target)return value and value*rtk.scale.framebuffer or 0 +end,},minh=rtk.Attribute{default=30,calculate=rtk.Reference('minw'),},maxw=rtk.Attribute{default=800,calculate=function(self,attr,value,target)return value and value*rtk.scale.framebuffer +end,},maxh=rtk.Attribute{default=600,calculate=rtk.Reference('maxw'),},visible=rtk.Attribute{window_sync=true,},docked=rtk.Attribute{default=false,window_sync=true,reflow=rtk.Widget.REFLOW_NONE,},dock=rtk.Attribute{default=rtk.Window.DOCK_RIGHT,calculate={bottom=rtk.Window.DOCK_BOTTOM,left=rtk.Window.DOCK_LEFT,top=rtk.Window.DOCK_TOP,right=rtk.Window.DOCK_RIGHT,floating=rtk.Window.DOCK_FLOATING +},window_sync=true,reflow=rtk.Widget.REFLOW_NONE,},pinned=rtk.Attribute{default=false,window_sync=true,calculate=function(self,attr,value,target)return rtk.has_js_reascript_api and value +end,},borderless=rtk.Attribute{default=false,window_sync=true,calculate=rtk.Reference('pinned')},[1]=rtk.Attribute{alias='title'},title=rtk.Attribute{default='REAPER application',reflow=rtk.Widget.REFLOW_NONE,window_sync=true,redraw=false,},opacity=rtk.Attribute{default=1.0,reflow=rtk.Widget.REFLOW_NONE,window_sync=true,redraw=false,},resizable=rtk.Attribute{default=true,reflow=rtk.Widget.REFLOW_NONE,window_sync=true,},hwnd=nil,in_window=false,is_focused=not rtk.has_js_reascript_api and true or false,running=false,cursor=rtk.mouse.cursors.POINTER,scalability=rtk.Widget.BOX,}function rtk.Window:initialize(attrs,...)rtk.Container.initialize(self,attrs,self.class.attributes.defaults,...)rtk.window=self +self.window=self +if self.id==0 and self.calc.bg and rtk.theme.default then +rtk.set_theme_by_bgcolor(self.calc.bg)end +if rtk.Window.static._icon_resize_grip==nil then +rtk.Window._make_icons()end +if not rtk.has_js_reascript_api then +self:sync('borderless', false)self:sync('pinned', false)end +self._dockstate=0 +self._backingstore=rtk.Image()self._event=rtk.Event()self._reflow_queued=false +self._reflow_widgets=nil +self._blits_queued=0 +self._draw_queued=false +self._mouse_refresh_queued=false +self._sync_window_attrs_on_update=false +self._resize_grip=nil +self._move_grip=nil +self._os_window_frame_width=0 +self._os_window_frame_height=0 +self._undocked_geometry=nil +self._unmaximized_geometry=nil +self._last_mousemove_time=nil +self._last_mouseup_time=0 +self._touch_scrolling={count=0}self._last_synced_attrs={}end +function rtk.Window:_handle_attr(attr,value,oldval,trigger,reflow,sync)local ok=rtk.Widget._handle_attr(self,attr,value,oldval,trigger,reflow,sync)if ok==false then +return ok +end +if attr=='bg' then +local color=rtk.color.int(value or rtk.theme.bg)gfx.clear=color +if rtk.has_js_reascript_api then +if self._gdi_brush then +reaper.JS_GDI_DeleteObject(self._gdi_brush)reaper.JS_GDI_DeleteObject(self._gdi_pen)else +reaper.atexit(function()reaper.JS_GDI_DeleteObject(self._gdi_brush)reaper.JS_GDI_DeleteObject(self._gdi_pen)end)end +color=rtk.color.flip_byte_order(color)self._gdi_brush=reaper.JS_GDI_CreateFillBrush(color)self._gdi_pen=reaper.JS_GDI_CreatePen(1,color)end +end +if self.class.attributes.get(attr).window_sync and not sync then +self._sync_window_attrs_on_update=true +end +return true +end +function rtk.Window:_get_dockstate_from_attrs()local calc=self.calc +local dock=calc.dock +if type(dock)=='table' then +dock=self:_get_docker_at_pos(dock[1])end +local dockstate=(dock or 0)<<8 +if calc.docked and calc.docked~=0 then +dockstate=dockstate|1 +end +return dockstate +end +function rtk.Window:_get_docker_at_pos(pos)if not reaper.DockGetPosition then +return 0 +end +for i=1,20 do +if reaper.DockGetPosition(i)==pos then +return i +end +end +end +function rtk.Window:_clear_gdi(startw,starth)if not rtk.os.windows or not rtk.has_js_reascript_api or not self.hwnd then +return +end +local calc=self.calc +local dc=reaper.JS_GDI_GetWindowDC(self.hwnd)reaper.JS_GDI_SelectObject(dc,self._gdi_brush)reaper.JS_GDI_SelectObject(dc,self._gdi_pen)local x=0 +local y=0 +local r,w,h=reaper.JS_Window_GetClientSize(self.hwnd)if not startw then +reaper.JS_GDI_FillRect(dc,x,y,w*2,h*2)elseif w>startw or h>starth then +if not calc.docked and not calc.borderless then +startw=startw+self._os_window_frame_width +starth=starth+self._os_window_frame_height +end +reaper.JS_GDI_FillRect(dc,x+math.round(startw),y,w*2,h*2)reaper.JS_GDI_FillRect(dc,x,y+math.round(starth),w*2,h*2)end +reaper.JS_GDI_ReleaseDC(self.hwnd,dc)end +function rtk.Window:focus()if self.hwnd and rtk.has_js_reascript_api then +reaper.JS_Window_SetFocus(self.hwnd)self:queue_draw()return true +else +return false +end +end +function rtk.Window:_run()self:_update()if self.running then +rtk.defer(self._run,self)end +end +function rtk.Window:_get_display_resolution(working)local x=math.floor(self.x)local y=math.floor(self.y)local w=math.floor(x+(self.w or 1))local h=math.floor(y+(self.h or 1))local l,t,r,b=reaper.my_getViewport(0,0,0,0,x,y,w,h,working and 1 or 0)return l,t,r-l,math.abs(b-t)end +function rtk.Window:_get_geometry_from_attrs(overrides)local scale=rtk.scale.framebuffer or 1 +local x=self.x +local y=self.y +local w=self.calc.w/scale +local h=self.calc.h/scale +if overrides then +local sx,sy,sw,sh=self:_get_display_resolution(true)if sw and sh then +if overrides.halign==rtk.Widget.LEFT then +x=sx +elseif overrides.halign==rtk.Widget.CENTER then +x=sx+(overrides.x or 0)+(sw-w)/2 +elseif overrides.halign==rtk.Widget.RIGHT then +x=sx+(overrides.x or 0)+(sw-w)end +if rtk.os.mac then +if overrides.valign==rtk.Widget.TOP then +y=sy+(overrides.y or 0)+(sh-h)elseif overrides.valign==rtk.Widget.CENTER then +y=sy+(overrides.y or 0)+(sh-h)/2 +elseif overrides.valign==rtk.Widget.BOTTOM then +y=sy+(overrides.y or 0)end +else +if overrides.valign==rtk.Widget.TOP then +y=sy +elseif overrides.valign==rtk.Widget.CENTER then +y=sy+(overrides.y or 0)+(sh-h)/2 +elseif overrides.valign==rtk.Widget.BOTTOM then +y=sy+(overrides.y or 0)+(sh-h)end +end +if overrides.constrain then +x=rtk.clamp(x,sx,sx+sw-w)y=rtk.clamp(y,sy,sy+sh-h)w=rtk.clamp(w,self.minw or 0,sw-(x-sx))h=rtk.clamp(h,self.minh or 0,sh-(rtk.os.mac and y-sy-h or y-sy))end +end +end +return math.round(x),math.round(y),math.round(w),math.round(h)end +function rtk.Window:_shrinkwrap()local calc=self.calc +if #self.children==0 then +calc.w=self.w and calc.w or calc.maxw +calc.h=self.h and calc.h or calc.maxh +else +self:reflow(rtk.Widget.REFLOW_FULL,not self.w,not self.h)end +end +function rtk.Window:_sync_window_attrs(overrides)local calc=self.calc +local lastw,lasth=self.w,self.h +local resized +local dockstate=self:_get_dockstate_from_attrs()if not rtk.has_js_reascript_api or not self.hwnd then +if dockstate~=self._dockstate then +gfx.dock(dockstate)self:_handle_dock_change(dockstate)self:onresize(lastw,lasth)return 1 +else +return 0 +end +end +if not self.w or not self.h then +self:_shrinkwrap()end +if dockstate~=self._dockstate then +gfx.dock(dockstate)local r,w,h=reaper.JS_Window_GetClientSize(self.hwnd)self:_handle_dock_change(dockstate)if calc.docked then +gfx.w,gfx.h=w,h +self:sync('w', w / rtk.scale.framebuffer, w)self:sync('h', h / rtk.scale.framebuffer, h)end +self:onresize(lastw,lasth)return 1 +end +if self._resize_grip then +self._resize_grip:attr('visible', calc.borderless and calc.resizable and not calc.docked)end +if not calc.docked then +if not calc.visible then +reaper.JS_Window_Show(self.hwnd, 'HIDE')return 0 +end +local style='SYSMENU,DLGSTYLE,BORDER,CAPTION'if calc.resizable then +style=style .. ',THICKFRAME'end +if calc.borderless then +style='POPUP'self:_setup_borderless()if not self.realized then +local sw=math.ceil(self.calc.w/rtk.scale.framebuffer)local sh=math.ceil(self.calc.h/rtk.scale.framebuffer)reaper.JS_Window_Resize(self.hwnd,sw,sh)end +end +local function restyle()reaper.JS_Window_SetStyle(self.hwnd,style)if rtk.os.bits~=32 then +local n=reaper.JS_Window_GetLong(self.hwnd, 'STYLE')reaper.JS_Window_SetLong(self.hwnd, 'STYLE', n | 0x80000000)end +reaper.JS_Window_SetZOrder(self.hwnd, calc.pinned and 'TOPMOST' or 'NOTOPMOST')local r,x1,y1,x2,y2=reaper.JS_Window_GetClientRect(self.hwnd)if r then +reaper.JS_Window_Resize(self.hwnd,x2-x1,y2-y1)self:_discover_os_window_frame_size(self.hwnd)end +end +if reaper.JS_Window_IsVisible(self.hwnd)then +restyle()else +rtk.defer(restyle)end +local x,y,w,h=self:_get_geometry_from_attrs(overrides)local scaled_gfxw=gfx.w/rtk.scale.framebuffer +local scaled_gfxh=gfx.h/rtk.scale.framebuffer +if not resized then +if w==scaled_gfxw and h==scaled_gfxh then +resized=0 +elseif w<=scaled_gfxw and h<=scaled_gfxh then +resized=-1 +elseif w>scaled_gfxw or h>scaled_gfxh then +resized=1 +end +end +local r,lastx,lasty,x2,y2=reaper.JS_Window_GetClientRect(self.hwnd)local moved=r and(self.x~=lastx or self.y~=lasty)local borderless_toggled=calc.borderless~=self._last_synced_attrs.borderless +if moved or resized~=0 or borderless_toggled then +local sw,sh=w,h +if not calc.borderless then +sw=w+self._os_window_frame_width/rtk.scale.framebuffer +sh=h+self._os_window_frame_height/rtk.scale.framebuffer +end +sw=math.ceil(sw)sh=math.ceil(sh)reaper.JS_Window_SetPosition(self.hwnd,x,y,sw,sh)end +if resized~=0 then +gfx.w=w*rtk.scale.framebuffer +gfx.h=h*rtk.scale.framebuffer +self:queue_blit()self:onresize(scaled_gfxw,scaled_gfxh)end +if moved then +self:sync('x', x, 0)self:sync('y', y, 0)self:onmove(lastx,lasty)end +reaper.JS_Window_SetOpacity(self.hwnd, 'ALPHA', calc.opacity)reaper.JS_Window_SetTitle(self.hwnd,calc.title)else +local flags=reaper.JS_Window_GetLong(self.hwnd, 'EXSTYLE')flags=flags&~0x00080000 +reaper.JS_Window_SetLong(self.hwnd, 'EXSTYLE', flags)end +self._last_synced_attrs.borderless=calc.borderless +return resized or 0 +end +function rtk.Window:open(options)if self.running or rtk._quit then +return +end +rtk.window=self +if options then +options.halign=options.halign or options.align +options.valign=options.valign or options.align +end +if not self.w or not self.h then +self:_shrinkwrap()end +local calc=self.calc +self.running=true +gfx.ext_retina=1 +self:_handle_attr('bg', calc.bg or rtk.theme.bg)options=self:_calc_cell_attrs(self,options)local x,y,w,h=self:_get_geometry_from_attrs(options)self:sync('x', x, 0)self:sync('y', y, 0)self:sync('w', w)self:sync('h', h)local dockstate=self:_get_dockstate_from_attrs()gfx.init(calc.title,calc.w/rtk.scale.framebuffer,calc.h/rtk.scale.framebuffer,dockstate,x,y)gfx.update()if gfx.ext_retina==2 and rtk.os.mac and rtk.scale.framebuffer~=2 then +log.warning('rtk.Window:open(): unexpected adjustment to rtk.scale.framebuffer: %s -> 2', rtk.scale.framebuffer)rtk.scale.framebuffer=2 +calc.w=calc.w*rtk.scale.framebuffer +calc.h=calc.h*rtk.scale.framebuffer +end +dockstate,_,_=gfx.dock(-1,true,true)self:_handle_dock_change(dockstate)if rtk.has_js_reascript_api then +self:_clear_gdi()else +rtk.color.set(rtk.theme.bg)gfx.rect(0,0,w,h,1)end +self._draw_queued=true +self:_run()end +function rtk.Window:_close()self.running=false +gfx.quit()end +function rtk.Window:close()self.running=false +self.hwnd=nil +gfx.quit()self:onclose()end +function rtk.Window:_setup_borderless()if self._move_grip then +return +end +local calc=self.calc +local move=rtk.Spacer{z=-10000,w=1.0,h=30,touch_activate_delay=0}move.onmousedown=function(this,event)if not calc.docked and calc.borderless then +local _,wx,wy,_,_=reaper.JS_Window_GetClientRect(self.hwnd)local mx,my=reaper.GetMousePosition()this._drag_start_mx=mx +this._drag_start_my=my +this._drag_start_wx=wx +this._drag_start_wy=wy +this._drag_start_ww=gfx.w/rtk.scale.framebuffer +this._drag_start_wh=gfx.h/rtk.scale.framebuffer +this._drag_start_dx=mx-wx +this._drag_start_dy=my-wy +end +return true +end +move.ondragstart=function(this,event)if not calc.docked and calc.borderless and this._drag_start_mx then +return true +else +return false +end +end +move.ondragend=function(this,event)this._drag_start_mx=nil +end +move.ondragmousemove=function(this,event)local _,wx,wy,_,wy2=reaper.JS_Window_GetClientRect(self.hwnd)local mx,my=reaper.GetMousePosition()local x=mx-this._drag_start_dx +local y +if rtk.os.mac then +local h=wy-wy2 +y=my-this._drag_start_dy-h +else +y=my-this._drag_start_dy +end +if self._unmaximized_geometry then +local _,_,w,h=table.unpack(self._unmaximized_geometry)local sx,_,sw,sh=self:_get_display_resolution()local xoffset=event.x/rtk.scale.framebuffer +local dx=math.ceil(w*xoffset/this._drag_start_ww)x=rtk.clamp(sx+xoffset-dx,sx,sx+sw-w)self._unmaximized_geometry=nil +this._drag_start_ww=w +this._drag_start_wh=h +this._drag_start_dx=dx +if rtk.os.mac then +y=(wy-h)+(my-this._drag_start_my)end +reaper.JS_Window_SetPosition(self.hwnd,x,y,w,h)else +reaper.JS_Window_Move(self.hwnd,x,y)end +end +move.ondoubleclick=function(this,event)if calc.docked or not calc.borderless then +return +end +local x,y,w,h=self:_get_display_resolution(true)if self._unmaximized_geometry then +if math.abs(w-self.w)>8)&0xff +self:sync('dock', calc.dock)self:sync('docked', calc.docked)self._dockstate=dockstate +self.hwnd=self:_get_hwnd()self:queue_reflow(rtk.Widget.REFLOW_FULL)if was_docked~=calc.docked then +self:_clear_gdi()if calc.docked then +self._undocked_geometry={self.x,self.y,self.w,self.h}elseif self._undocked_geometry then +local x,y,w,h=table.unpack(self._undocked_geometry)local gw=w*rtk.scale.framebuffer +local gh=h*rtk.scale.framebuffer +self:sync('x', x, 0)self:sync('y', y, 0)self:sync('w', w, gw)self:sync('h', h, gh)gfx.w=gw +gfx.h=gh +end +end +self:_sync_window_attrs()self:queue_blit()self:ondock()end +function rtk.Window:queue_reflow(mode,widget)if mode~=rtk.Widget.REFLOW_FULL and widget and widget.box then +if self._reflow_widgets then +self._reflow_widgets[widget]=true +elseif not self._reflow_queued then +self._reflow_widgets={[widget]=true}end +else +self._reflow_widgets=nil +end +self._reflow_queued=true +end +function rtk.Window:queue_draw()self._draw_queued=true +end +function rtk.Window:queue_blit()self._blits_queued=self._blits_queued+2 +end +function rtk.Window:_get_content_size(boxw,boxh,fillw,fillh,clampw,clamph,scale)local tp,rp,bp,lp=self:_get_padding_and_border()local calc=self.calc +return self.w and(calc.w-lp-rp)or nil,self.h and(calc.h-tp-bp)or nil,tp,rp,bp,lp +end +function rtk.Window:queue_mouse_refresh()self._mouse_refresh_queued=true +end +function rtk.Window:_reflow(boxx,boxy,boxw,boxh,fillw,filly,clampw,clamph,uiscale,viewport,window)rtk.Container._reflow(self,boxx,boxy,boxw,boxh,fillw,filly,clampw,clamph,uiscale,viewport,window)self.calc.x=0 +self.calc.y=0 +end +function rtk.Window:reflow(mode,shrinkwrapw,shrinkwraph)local calc=self.calc +local widgets=self._reflow_widgets +local full=false +self._reflow_queued=false +self._reflow_widgets=nil +local t0=reaper.time_precise()if mode~=rtk.Widget.REFLOW_FULL and widgets and self.realized and #widgets<20 then +for widget,_ in pairs(widgets)do +widget:reflow()widget:_realize_geometry()end +else +local saved_size +local boxw,boxh=calc.w,calc.h +if shrinkwrapw or shrinkwraph then +saved_size={self.w,self.h}local _,_,sw,sh=self:_get_display_resolution(true)boxw=shrinkwrapw and(calc.maxw or sw*rtk.scale.framebuffer)or calc.w +boxh=shrinkwraph and(calc.maxh or sh*rtk.scale.framebuffer)or calc.h +self.w=not shrinkwrapw and self.w or nil +self.h=not shrinkwraph and self.h or nil +end +local _,_,w,h=rtk.Container.reflow(self,0,0,boxw,boxh,nil,nil,true,true,rtk.scale.value,nil,self +)self:_realize_geometry()full=true +if saved_size then +self.w,self.h=table.unpack(saved_size)end +end +local reflow_time=reaper.time_precise()-t0 +if reflow_time>0.02 then +log.warning("rtk: slow reflow: %s", reflow_time)end +self:onreflow(widgets)self._draw_queued=true +return full +end +function rtk.Window:_get_mouse_button_event(bit,type)if not type then +if rtk.mouse.down&bit==0 and gfx.mouse_cap&bit~=0 then +rtk.mouse.down=rtk.mouse.down|bit +type=rtk.Event.MOUSEDOWN +elseif rtk.mouse.down&bit~=0 and gfx.mouse_cap&bit==0 then +rtk.mouse.down=rtk.mouse.down&~bit +type=rtk.Event.MOUSEUP +end +end +if type then +local event=self._event:reset(type)event.x,event.y=gfx.mouse_x,gfx.mouse_y +event:set_modifiers(gfx.mouse_cap,bit)return event +end +end +function rtk.Window:_get_mousemove_event(simulated)local event=self._event:reset(rtk.Event.MOUSEMOVE)event.simulated=simulated +event:set_modifiers(gfx.mouse_cap,rtk.mouse.state.latest or 0)return event +end +local function _get_wheel_distance(v)if rtk.os.mac then +return-v/90 +else +return-v/120 +end +end +function rtk.Window:_update()local calc=self.calc +local now=reaper.time_precise()local need_draw=false +if gfx.ext_retina~=rtk.scale.system then +rtk.scale.system=gfx.ext_retina +rtk.scale._calc()self:queue_reflow()end +local files=nil +local _,fname=gfx.getdropfile(0)if fname then +files={fname}local idx=1 +while true do +_,fname=gfx.getdropfile(idx)if not fname then +break +end +files[#files+1]=fname +idx=idx+1 +end +gfx.getdropfile(-1)end +gfx.update()if rtk._soon_funcs then +rtk._run_soon()end +local focus_changed=false +if rtk.has_js_reascript_api then +rtk.focused_hwnd=reaper.JS_Window_GetFocus()local is_focused=self.hwnd==rtk.focused_hwnd +if is_focused~=self.is_focused then +self.is_focused=is_focused +need_draw=true +focus_changed=true +end +end +if self:onupdate()==false then +return +end +need_draw=rtk._do_animations(now)or need_draw +if self._sync_window_attrs_on_update then +if self:_sync_window_attrs()~=0 then +self:reflow(rtk.Widget.REFLOW_FULL)need_draw=true +end +self._sync_window_attrs_on_update=false +end +local dockstate,x,y=gfx.dock(-1,true,true)local dock_changed=dockstate~=self._dockstate +if dock_changed then +self:_handle_dock_change(dockstate)end +if x~=self.x or y~=self.y then +local lastx,lasty=self.x,self.y +self:sync('x', x, 0)self:sync('y', y, 0)self:onmove(lastx,lasty)end +local resized=gfx.w~=calc.w or gfx.h~=calc.h +if resized and self.visible then +local last_w,last_h=self.w,self.h +self:sync('w', gfx.w / rtk.scale.framebuffer, gfx.w)self:sync('h', gfx.h / rtk.scale.framebuffer, gfx.h)self:_clear_gdi(calc.w,calc.h)self:onresize(last_w,last_h)self:reflow(rtk.Widget.REFLOW_FULL)need_draw=true +elseif self._reflow_queued then +self:reflow()need_draw=true +end +local event=nil +local last_cursor=calc.cursor +calc.cursor=rtk.mouse.cursors.UNDEFINED +if gfx.mouse_wheel~=0 or gfx.mouse_hwheel~=0 then +event=self._event:reset(rtk.Event.MOUSEWHEEL)event:set_modifiers(gfx.mouse_cap,0)event.wheel=_get_wheel_distance(gfx.mouse_wheel)event.hwheel=_get_wheel_distance(gfx.mouse_hwheel)self:onmousewheel(event)gfx.mouse_wheel=0 +gfx.mouse_hwheel=0 +self:_handle_window_event(event,now)end +local char=gfx.getchar()if char>0 then +event=self._event:reset(rtk.Event.KEY)event:set_modifiers(gfx.mouse_cap,0)event.char=nil +event.keycode=char +if char<=26 and event.ctrl then +event.char=string.char(char+96)elseif char>=32 and char~=127 then +if char<=255 then +event.char=string.char(char)elseif char<=282 then +event.char=string.char(char-160)elseif char<=346 then +event.char=string.char(char-224)end +end +self:onkeypresspre(event)self:_handle_window_event(event,now)elseif char<0 then +self:close()end +if files then +event=self:_get_mousemove_event(false)event.type=rtk.Event.DROPFILE +event.files=files +self:_handle_window_event(event,now)end +rtk._touch_activate_event=rtk.touchscroll and rtk.Event.MOUSEUP or rtk.Event.MOUSEDOWN +need_draw=need_draw or self._draw_queued +local mouse_button_changed=(rtk.mouse.down~=gfx.mouse_cap&rtk.mouse.BUTTON_MASK)local buttons_down=(gfx.mouse_cap&rtk.mouse.BUTTON_MASK~=0)local mouse_moved=(rtk.mouse.x~=gfx.mouse_x or rtk.mouse.y~=gfx.mouse_y)local last_in_window=self.in_window +self.in_window=gfx.mouse_x>=0 and gfx.mouse_y>=0 and gfx.mouse_x<=gfx.w and gfx.mouse_y<=gfx.h +local in_window_changed=self.in_window~=last_in_window +if self._last_mousemove_time and rtk._mouseover_widget and +rtk._mouseover_widget~=self._tooltip_widget and +now-self._last_mousemove_time>rtk.tooltip_delay then +self._tooltip_widget=rtk._mouseover_widget +need_draw=true +end +if mouse_button_changed and rtk.touchscroll and self._jsx then +self._restore_mouse_pos={self._jsx,self._jsy}end +if mouse_moved then +if self.in_window then +self._jsx=nil +elseif not buttons_down then +self._jsx,self._jsy=reaper.GetMousePosition()end +if self._mouse_refresh_queued then +self._mouse_refresh_queued=false +local tmp=self:_get_mousemove_event(true)tmp.buttons=0 +tmp.button=0 +self:_handle_window_event(tmp,now)need_draw=true +end +end +if not event or mouse_moved then +local suppress=false +if self.in_window and rtk.has_js_reascript_api and self.hwnd then +local x,y=reaper.GetMousePosition()local hwnd=reaper.JS_Window_FromPoint(x,y)if hwnd~=self.hwnd then +self.in_window=false +in_window_changed=last_in_window~=false +end +end +if need_draw or(mouse_moved and self.in_window)or in_window_changed or +(rtk.dnd.dragging and buttons_down)then +event=self:_get_mousemove_event(not mouse_moved)if buttons_down and rtk.touchscroll and not rtk.dnd.dragging then +suppress=not event:get_button_state('mousedown-handled')end +elseif rtk.mouse.down~=0 and not mouse_button_changed then +local buttonstate=rtk.mouse.state[rtk.mouse.state.latest] +local wait=math.max(rtk.long_press_delay,rtk.touch_activate_delay)if now-buttonstate.time<=wait+(2/rtk.fps)then +event=self:_get_mouse_button_event(rtk.mouse.state.latest,rtk.Event.MOUSEDOWN)event.simulated=true +end +end +if event and(not event.simulated or self._touch_scrolling.count==0 or buttons_down)then +need_draw=need_draw or self._tooltip_widget~=nil +self:_handle_window_event(event,now,suppress)end +end +rtk.mouse.x=gfx.mouse_x +rtk.mouse.y=gfx.mouse_y +if mouse_button_changed then +event=self:_get_mouse_button_event(rtk.mouse.BUTTON_LEFT)if not event then +event=self:_get_mouse_button_event(rtk.mouse.BUTTON_RIGHT)if not event then +event=self:_get_mouse_button_event(rtk.mouse.BUTTON_MIDDLE)end +end +if event then +if event.type==rtk.Event.MOUSEDOWN then +local buttonstate=rtk.mouse.state[event.button] +if not buttonstate then +buttonstate={}rtk.mouse.state[event.button]=buttonstate +end +buttonstate.time=now +rtk.mouse.state.order[#rtk.mouse.state.order+1]=event.button +rtk.mouse.state.latest=event.button +elseif event.type==rtk.Event.MOUSEUP then +for i=1,#rtk.mouse.state.order do +if rtk.mouse.state.order[i]==event.button then +table.remove(rtk.mouse.state.order,i)break +end +end +if #rtk.mouse.state.order>0 then +rtk.mouse.state.latest=rtk.mouse.state.order[#rtk.mouse.state.order] +else +rtk.mouse.state.latest=0 +end +if rtk.touchscroll and event.buttons==0 and self._restore_mouse_pos then +local x,y=table.unpack(self._restore_mouse_pos)rtk.callafter(0.2,reaper.JS_Mouse_SetPosition,x,y)self._restore_mouse_pos=nil +end +end +self:_handle_window_event(event,now)else +log.warning('rtk: no event for mousecap=%s which indicates an internal rtk bug', gfx.mouse_cap)end +end +if rtk._soon_funcs then +rtk._run_soon()end +local blitted=false +if event and calc.visible then +if self._reflow_queued and not self._sync_window_attrs_on_update then +if self:reflow()then +calc.cursor=rtk.mouse.cursors.UNDEFINED +self:_handle_window_event(self:_get_mousemove_event(true),now)end +end +if need_draw or self._draw_queued then +self._backingstore:resize(calc.w,calc.h,false)self._backingstore:pushdest()self:clear()self._draw_queued=false +self:_draw(0,0,calc.alpha,event,calc.w,calc.h,0,0,0,0)if event.debug then +event.debug:_draw_debug_info(event)end +if self._tooltip_widget and not rtk.dnd.dragging then +self._tooltip_widget:_draw_tooltip(rtk.mouse.x,rtk.mouse.y,calc.w,calc.h)end +self._backingstore:popdest()self:_blit()blitted=true +end +if focus_changed then +if self.is_focused then +if self._focused_saved then +self._focused_saved:focus(event)self._focused_saved=nil +end +self:onfocus(event)else +if rtk.focused then +self._focused_saved=rtk.focused +rtk.focused:blur(event,nil)end +self:onblur(event)end +end +if not event.handled and rtk.is_modal()and +((focus_changed and not self.is_focused)or event.type==rtk._touch_activate_event)then +for _,widget in pairs(rtk._modal)do +widget:_release_modal(event)end +end +if not event.handled and rtk.focused and event.type==rtk._touch_activate_event then +rtk.focused:blur(event,nil)end +if event.type==rtk.Event.KEY then +self:onkeypresspost(event)if event.handled then +return +end +if event.keycode==rtk.keycodes.F12 and log.level<=log.DEBUG then +rtk.debug=not rtk.debug +self:queue_draw()elseif event.keycode==rtk.keycodes.ESCAPE and not self.docked then +self:close()end +end +if calc.cursor==rtk.mouse.cursors.UNDEFINED then +calc.cursor=self.cursor +end +if self.in_window then +if type(calc.cursor)=='userdata' then +reaper.JS_Mouse_SetCursor(calc.cursor)reaper.JS_WindowMessage_Intercept(self.hwnd, "WM_SETCURSOR", false)else +gfx.setcursor(calc.cursor,0)end +elseif in_window_changed and self.hwnd and rtk.has_js_reascript_api then +reaper.JS_WindowMessage_Release(self.hwnd, "WM_SETCURSOR")end +end +if mouse_moved then +self._last_mousemove_time=now +end +if self._blits_queued>0 then +if not blitted then +self:_blit()end +self._blits_queued=self._blits_queued-1 +end +local duration=reaper.time_precise()-now +if duration>0.04 then +log.debug("rtk: very slow update: %s event=%s", duration, event)end +end +function rtk.Window:_blit()self._backingstore:blit{mode=rtk.Image.FAST_BLIT}end +function rtk.Window:_handle_window_event(event,now,suppress)if not self.calc.visible then +return +end +if not event.simulated then +rtk._mouseover_widget=nil +self._tooltip_widget=nil +self._last_mousemove_time=nil +end +event.time=now +if not suppress then +rtk.Container._handle_event(self,0,0,event,false,rtk._modal==nil)end +assert(event.type~=rtk.Event.MOUSEDOWN or event.button~=0)if event.type==rtk.Event.MOUSEUP then +rtk.mouse.state[event.button]=nil +if event.buttons==0 then +rtk._pressed_widgets=nil +end +self._last_mouseup_time=event.time +rtk._drag_candidates=nil +if rtk.dnd.dropping then +rtk.dnd.dropping:_handle_dropblur(event,rtk.dnd.dragging,rtk.dnd.arg)rtk.dnd.dropping=nil +end +if rtk.dnd.dragging and event.buttons&rtk.dnd.buttons==0 then +rtk.dnd.dragging:_handle_dragend(event,rtk.dnd.arg)rtk.dnd.dragging=nil +rtk.dnd.arg=nil +local tmp=event:clone{type=rtk.Event.MOUSEMOVE,simulated=true}rtk.Container._handle_event(self,0,0,tmp,false,rtk._modal==nil)end +elseif rtk._drag_candidates and event.type==rtk.Event.MOUSEMOVE and +not event.simulated and event.buttons~=0 and not rtk.dnd.arg then +event.handled=nil +rtk.dnd.droppable=true +local missed=false +local dthresh=math.ceil(rtk.scale.value ^ 1.7)if rtk.touchscroll and event.time-self._last_mouseup_time<0.2 then +dthresh=rtk.scale.value*10 +end +for n,state in ipairs(rtk._drag_candidates)do +local widget,offered=table.unpack(state)if not offered then +local ex,ey,when=table.unpack(rtk._pressed_widgets[widget.id])local dx=math.abs(ex-event.x)local dy=math.abs(ey-event.y)local tthresh=widget:_get_touch_activate_delay(event)if event.time-when>=tthresh and(dx>dthresh or dy>dthresh)then +local arg,droppable=widget:_handle_dragstart(event,ex,ey,when)if arg then +widget:_deferred_mousedown(event,ex,ey)rtk.dnd.dragging=widget +rtk.dnd.arg=arg +rtk.dnd.droppable=droppable~=false and true or false +rtk.dnd.buttons=event.buttons +widget:_handle_dragmousemove(event,arg)break +elseif event.handled then +break +end +state[2]=true +else +missed=true +end +end +end +if not missed or event.handled then +rtk._drag_candidates=nil +end +end +end +function rtk.Window:request_mouse_cursor(cursor,force)if cursor and(self.calc.cursor==rtk.mouse.cursors.UNDEFINED or force)then +self.calc.cursor=cursor +return true +else +return false +end +end +function rtk.Window:clear()self._backingstore:clear(self.calc.bg or rtk.theme.bg)end +function rtk.Window:get_normalized_y()if not rtk.os.mac then +return self.y +else +local _,_,_,sh=self:_get_display_resolution()local offset=gfx.h+self._os_window_frame_height +return sh-self.y-offset/rtk.scale.framebuffer +end +end +function rtk.Window:_set_touch_scrolling(viewport,state)local ts=self._touch_scrolling +local exists=ts[viewport.id]~=nil +if state and not exists then +ts[viewport.id]=viewport +ts.count=ts.count+1 +elseif not state and exists then +ts[viewport.id]=nil +ts.count=ts.count-1 +end +end +function rtk.Window:_is_touch_scrolling(viewport)if viewport then +return self._touch_scrolling[viewport.id]~=nil +else +return self._touch_scrolling.count>0 +end +end +function rtk.Window:onupdate()end +function rtk.Window:onreflow(widgets)end +function rtk.Window:onmove(lastx,lasty)end +function rtk.Window:onresize(lastw,lasth)end +function rtk.Window:ondock()end +function rtk.Window:onclose()end +function rtk.Window:onkeypresspre(event)end +function rtk.Window:onkeypresspost(event)end +end)() + +__mod_rtk_box=(function() +local rtk=__mod_rtk_core +local log=__mod_rtk_log +rtk.Box=rtk.class('rtk.Box', rtk.Container)rtk.Box.static.HORIZONTAL=1 +rtk.Box.static.VERTICAL=2 +rtk.Box.static.FLEXSPACE={}rtk.Box.static.STRETCH_NONE=0 +rtk.Box.static.STRETCH_FULL=1 +rtk.Box.static.STRETCH_TO_SIBLINGS=2 +rtk.Box.register{expand=rtk.Attribute{type='number'},fillw=false,fillh=false,stretch=rtk.Attribute{calculate={none=rtk.Box.STRETCH_NONE,full=rtk.Box.STRETCH_FULL,siblings=rtk.Box.STRETCH_TO_SIBLINGS,['true']=rtk.Box.STRETCH_FULL,['false']=rtk.Box.STRETCH_NONE,[true]=rtk.Box.STRETCH_FULL,[false]=rtk.Box.STRETCH_NONE,[rtk.Attribute.NIL]=rtk.Box.STRETCH_NONE,}},bg=nil,orientation=nil,spacing=rtk.Attribute{default=0,reflow=rtk.Widget.REFLOW_FULL,},}function rtk.Box:initialize(attrs,...)rtk.Container.initialize(self,attrs,self.class.attributes.defaults,...)assert(self.orientation, 'rtk.Box cannot be instantiated directly, use rtk.HBox or rtk.VBox instead')end +function rtk.Box:_validate_child(child)if child~=rtk.Box.FLEXSPACE then +rtk.Container._validate_child(self,child)end +end +function rtk.Box:_reflow(boxx,boxy,boxw,boxh,fillw,fillh,clampw,clamph,uiscale,viewport,window)local calc=self.calc +calc.x,calc.y=self:_get_box_pos(boxx,boxy)local w,h,tp,rp,bp,lp=self:_get_content_size(boxw,boxh,fillw,fillh,clampw,clamph)local inner_maxw=w or(boxw-lp-rp)local inner_maxh=h or(boxh-tp-bp)clampw=clampw or w~=nil or fillw +clamph=clamph or h~=nil or fillh +self._reflowed_children={}self._child_index_by_id={}local innerw,innerh,expand_unit_size,expw,exph=self:_reflow_step1(inner_maxw,inner_maxh,clampw,clamph,uiscale,viewport,window +)if self.orientation==rtk.Box.HORIZONTAL then +expw=(expand_unit_size>0)or expw +elseif self.orientation==rtk.Box.VERTICAL then +exph=(expand_unit_size>0)or exph +end +innerw,innerh=self:_reflow_step2(inner_maxw,inner_maxh,innerw,innerh,clampw,clamph,expand_unit_size,uiscale,viewport,window,tp,rp,bp,lp +)fillw=fillw or(self.w and tonumber(self.w)<1.0)fillh=fillh or(self.h and tonumber(self.h)<1.0)innerw=w or math.max(innerw,fillw and inner_maxw or 0)innerh=h or math.max(innerh,fillh and inner_maxh or 0)calc.w=innerw+lp+rp +calc.h=innerh+tp+bp +return expw,exph +end +function rtk.Box:_reflow_step1(w,h,clampw,clamph,uiscale,viewport,window)local calc=self.calc +local orientation=calc.orientation +local remaining_size=orientation==rtk.Box.HORIZONTAL and w or h +local expand_units=0 +local maxw,maxh=0,0 +local spacing=0 +local expw,exph=false,false +for n,widgetattrs in ipairs(self.children)do +local widget,attrs=table.unpack(widgetattrs)local wcalc=widget.calc +attrs._cellbox=nil +if widget.id then +self._child_index_by_id[widget.id]=n +end +if widget==rtk.Box.FLEXSPACE then +expand_units=expand_units+(attrs.expand or 1)spacing=0 +elseif widget.visible==true then +attrs._halign=attrs.halign or calc.halign +attrs._valign=attrs.valign or calc.valign +local implicit_expand +if orientation==rtk.Box.HORIZONTAL then +implicit_expand=attrs.fillw +else +implicit_expand=attrs.fillh +end +attrs._calculated_expand=attrs.expand or(implicit_expand and 1)or 0 +if attrs._calculated_expand==0 and implicit_expand then +log.error('rtk.Box: %s: fill=true overrides explicit expand=0: %s will be expanded', self, widget)end +if attrs._calculated_expand==0 then +local ww,wh=0,0 +local wexpw,wexph +local ctp,crp,cbp,clp=self:_get_cell_padding(widget,attrs)if orientation==rtk.Box.HORIZONTAL then +local child_maxw=rtk.clamprel(remaining_size-clp-crp-spacing,attrs.minw or wcalc.minw,attrs.maxw or wcalc.maxw +)local child_maxh=rtk.clamprel(h-ctp-cbp,attrs.minh or wcalc.minh,attrs.maxh or wcalc.maxh +)_,_,ww,wh,wexpw,wexph=widget:reflow(0,0,child_maxw,child_maxh,attrs.fillw,attrs.fillh and attrs.stretch~=rtk.Box.STRETCH_TO_SIBLINGS,clampw,clamph,uiscale,viewport,window +)expw=wexpw or expw +exph=wexph or exph +ww=math.max(ww,attrs.minw or widget.minw or 0)wh=math.max(wh,attrs.minh or widget.minh or 0)if wexpw and clampw and ww>=child_maxw and n<#self.children then +attrs._calculated_expand=1 +end +else +local child_maxw=rtk.clamprel(w-clp-crp,attrs.minw or wcalc.minw,attrs.maxw or wcalc.maxw +)local child_maxh=rtk.clamprel(remaining_size-ctp-cbp-spacing,attrs.minh or wcalc.minh,attrs.maxh or wcalc.maxh +)_,_,ww,wh,wexpw,wexph=widget:reflow(0,0,child_maxw,child_maxh,attrs.fillw and attrs.stretch~=rtk.Box.STRETCH_TO_SIBLINGS,attrs.fillh,clampw,clamph,uiscale,viewport,window +)expw=wexpw or expw +exph=wexph or exph +wh=math.max(wh,attrs.minh or widget.minh or 0)ww=math.max(ww,attrs.minw or widget.minw or 0)if wexph and clamph and wh>=child_maxh and n<#self.children then +attrs._calculated_expand=1 +end +end +expw=expw or attrs.fillw +exph=exph or attrs.fillh +if attrs._calculated_expand==0 and wcalc.position&rtk.Widget.POSITION_INFLOW~=0 then +maxw=math.max(maxw,ww+clp+crp)maxh=math.max(maxh,wh+ctp+cbp)if orientation==rtk.Box.HORIZONTAL then +remaining_size=remaining_size-(clampw and(ww+clp+crp+spacing)or 0)else +remaining_size=remaining_size-(clamph and(wh+ctp+cbp+spacing)or 0)end +else +expand_units=expand_units+attrs._calculated_expand +end +else +expand_units=expand_units+attrs._calculated_expand +end +if orientation==rtk.Box.VERTICAL and attrs.stretch==rtk.Box.STRETCH_FULL then +maxw=w +elseif orientation==rtk.Box.HORIZONTAL and attrs.stretch==rtk.Box.STRETCH_FULL then +maxh=h +end +spacing=(attrs.spacing or self.spacing)*rtk.scale.value +self:_add_reflowed_child(widgetattrs,attrs.z or wcalc.z or 0)else +widget.realized=false +end +end +self:_determine_zorders()local expand_unit_size=expand_units>0 and(remaining_size/expand_units)or 0 +return maxw,maxh,expand_unit_size,expw,exph +end +end)() + +__mod_rtk_vbox=(function() +local rtk=__mod_rtk_core +rtk.VBox=rtk.class('rtk.VBox', rtk.Box)rtk.VBox.register{orientation=rtk.Box.VERTICAL +}function rtk.VBox:initialize(attrs,...)rtk.Box.initialize(self,attrs,self.class.attributes.defaults,...)end +function rtk.VBox:_reflow_step2(w,h,maxw,maxh,clampw,clamph,expand_unit_size,uiscale,viewport,window,tp,rp,bp,lp)local offset=0 +local spacing=0 +local second_pass={}for n,widgetattrs in ipairs(self.children)do +local widget,attrs=table.unpack(widgetattrs)local wcalc=widget.calc +if widget==rtk.Box.FLEXSPACE then +local previous=offset +offset=offset+expand_unit_size*(attrs.expand or 1)spacing=0 +maxh=math.max(maxh,offset)self:_set_cell_box(attrs,lp,tp+previous,maxw,offset-previous)elseif widget.visible==true then +local wx,wy,ww,wh +local ctp,crp,cbp,clp=self:_get_cell_padding(widget,attrs)local need_second_pass=(attrs.stretch==rtk.Box.STRETCH_TO_SIBLINGS or +(attrs._halign and attrs._halign~=rtk.Widget.LEFT and +not attrs.fillw and +attrs.stretch~=rtk.Box.STRETCH_FULL))local offx=lp+clp +local offy=offset+tp+ctp+spacing +local expand=attrs._calculated_expand +if expand and expand>0 then +local child_maxw=rtk.clamprel(w-clp-crp,attrs.minw or wcalc.minw,attrs.maxw or wcalc.maxw +)local child_maxh=rtk.clamprel((expand_unit_size*expand)-ctp-cbp-spacing,attrs.minh or wcalc.minh,attrs.maxh or wcalc.maxh +)wx,wy,ww,wh=widget:reflow(0,0,child_maxw,child_maxh,attrs.fillw,attrs.fillh,clampw,clamph,uiscale,viewport,window +)if attrs.stretch==rtk.Box.STRETCH_FULL then +ww=maxw +end +if need_second_pass then +second_pass[#second_pass+1]={widget,attrs,offx,offy,ww,child_maxh,ctp,crp,cbp,clp,offset,spacing +}else +self:_align_child(widget,attrs,offx,offy,ww,child_maxh,crp,cbp)self:_set_cell_box(attrs,lp,tp+offset+spacing,ww+clp+crp,child_maxh+ctp+cbp)end +wh=math.max(child_maxh,wh)else +ww=attrs.stretch==rtk.Box.STRETCH_FULL and maxw or wcalc.w +wh=math.max(wcalc.h,attrs.minh or wcalc.minh or 0)if need_second_pass then +second_pass[#second_pass+1]={widget,attrs,offx,offy,ww,wh,ctp,crp,cbp,clp,offset,spacing +}else +self:_align_child(widget,attrs,offx,offy,ww,wh,crp,cbp)self:_set_cell_box(attrs,lp,tp+offset+spacing,ww+clp+crp,wh+ctp+cbp)end +end +if wcalc.position&rtk.Widget.POSITION_INFLOW~=0 then +offset=offset+spacing+ctp+wh+cbp +end +maxw=math.max(maxw,ww+clp+crp)maxh=math.max(maxh,offset)spacing=(attrs.spacing or self.spacing)*rtk.scale.value +if not need_second_pass then +widget:_realize_geometry()end +end +end +if #second_pass>0 then +for n,widgetinfo in ipairs(second_pass)do +local widget,attrs,offx,offy,ww,child_maxh,ctp,crp,cbp,clp,offset,spacing=table.unpack(widgetinfo)if attrs.stretch==rtk.Box.STRETCH_TO_SIBLINGS then +wx,wy,ww,wh=widget:reflow(0,0,maxw,child_maxh,attrs.fillw,attrs.fillh,clampw,clamph,uiscale,viewport,window +)end +self:_align_child(widget,attrs,offx,offy,maxw,child_maxh,crp,cbp)self:_set_cell_box(attrs,lp,tp+offset+spacing,maxw+clp+crp,child_maxh+ctp+cbp)widget:_realize_geometry()end +end +return maxw,maxh +end +function rtk.VBox:_align_child(widget,attrs,offx,offy,cellw,cellh,crp,cbp)local x,y=offx,offy +local wcalc=widget.calc +if cellh>wcalc.h then +if attrs._valign==rtk.Widget.BOTTOM then +y=(offy-cbp)+cellh-wcalc.h-cbp +elseif attrs._valign==rtk.Widget.CENTER then +y=offy+(cellh-wcalc.h)/2 +end +end +if attrs._halign==rtk.Widget.CENTER then +x=(offx-crp)+(cellw-wcalc.w)/2 +elseif attrs._halign==rtk.Widget.RIGHT then +x=offx+cellw-wcalc.w-crp +end +wcalc.x=wcalc.x+x +widget.box[1]=x +wcalc.y=wcalc.y+y +widget.box[2]=y +end +end)() + +__mod_rtk_hbox=(function() +local rtk=__mod_rtk_core +rtk.HBox=rtk.class('rtk.HBox', rtk.Box)rtk.HBox.register{orientation=rtk.Box.HORIZONTAL +}function rtk.HBox:initialize(attrs,...)rtk.Box.initialize(self,attrs,self.class.attributes.defaults,...)end +function rtk.HBox:_reflow_step2(w,h,maxw,maxh,clampw,clamph,expand_unit_size,uiscale,viewport,window,tp,rp,bp,lp)local offset=0 +local spacing=0 +local second_pass={}for n,widgetattrs in ipairs(self.children)do +local widget,attrs=table.unpack(widgetattrs)local wcalc=widget.calc +if widget==rtk.Box.FLEXSPACE then +local previous=offset +offset=offset+expand_unit_size*(attrs.expand or 1)spacing=0 +maxw=math.max(maxw,offset)self:_set_cell_box(attrs,lp+previous,tp,offset-previous,maxh)elseif widget.visible==true then +local wx,wy,ww,wh +local ctp,crp,cbp,clp=self:_get_cell_padding(widget,attrs)local need_second_pass=(attrs.stretch==rtk.Box.STRETCH_TO_SIBLINGS or +(attrs._valign and attrs._valign~=rtk.Widget.TOP and +not attrs.fillh and +attrs.stretch~=rtk.Box.STRETCH_FULL))local offx=offset+lp+clp+spacing +local offy=tp+ctp +local expand=attrs._calculated_expand +if expand and expand>0 then +local child_maxw=rtk.clamprel((expand_unit_size*expand)-clp-crp-spacing,attrs.minw or wcalc.minw,attrs.maxw or wcalc.maxw +)local child_maxh=rtk.clamprel(h-ctp-cbp,attrs.minh or wcalc.minh,attrs.maxh or wcalc.maxh +)wx,wy,ww,wh=widget:reflow(0,0,child_maxw,child_maxh,attrs.fillw,attrs.fillh,clampw,clamph,uiscale,viewport,window +)if attrs.stretch==rtk.Box.STRETCH_FULL then +wh=maxh +end +if need_second_pass then +second_pass[#second_pass+1]={widget,attrs,offx,offy,child_maxw,wh,ctp,crp,cbp,clp,offset,spacing +}else +self:_align_child(widget,attrs,offx,offy,child_maxw,wh,crp,cbp)self:_set_cell_box(attrs,lp+offset+spacing,tp,child_maxw+clp+crp,wh+ctp+cbp)end +ww=math.max(child_maxw,ww)else +ww=math.max(wcalc.w,attrs.minw or wcalc.minw or 0)wh=attrs.stretch==rtk.Box.STRETCH_FULL and maxh or wcalc.h +if need_second_pass then +second_pass[#second_pass+1]={widget,attrs,offx,offy,ww,wh,ctp,crp,cbp,clp,offset,spacing +}else +self:_align_child(widget,attrs,offx,offy,ww,wh,crp,cbp)self:_set_cell_box(attrs,lp+offset+spacing,tp,ww+clp+crp,wh+ctp+cbp)end +end +if wcalc.position&rtk.Widget.POSITION_INFLOW~=0 then +offset=offset+spacing+clp+ww+crp +end +maxw=math.max(maxw,offset)maxh=math.max(maxh,wh+ctp+cbp)spacing=(attrs.spacing or self.spacing)*rtk.scale.value +if not need_second_pass then +widget:_realize_geometry()end +end +end +if #second_pass>0 then +for n,widgetinfo in ipairs(second_pass)do +local widget,attrs,offx,offy,child_maxw,wh,ctp,crp,cbp,clp,offset,spacing=table.unpack(widgetinfo)if attrs.stretch==rtk.Box.STRETCH_TO_SIBLINGS then +wx,wy,ww,wh=widget:reflow(0,0,child_maxw,maxh,attrs.fillw,attrs.fillh,clampw,clamph,uiscale,viewport,window +)end +self:_align_child(widget,attrs,offx,offy,child_maxw,maxh,crp,cbp)self:_set_cell_box(attrs,lp+offset+spacing,tp,child_maxw+clp+crp,maxh+ctp+cbp)widget:_realize_geometry()end +end +return maxw,maxh +end +function rtk.HBox:_align_child(widget,attrs,offx,offy,cellw,cellh,crp,cbp)local x,y=offx,offy +local wcalc=widget.calc +if cellw>wcalc.w then +if attrs._halign==rtk.Widget.RIGHT then +x=(offx-crp)+cellw-wcalc.w-crp +elseif attrs._halign==rtk.Widget.CENTER then +x=offx+(cellw-wcalc.w)/2 +end +end +if attrs._valign==rtk.Widget.CENTER then +y=(offy-cbp)+(cellh-wcalc.h)/2 +elseif attrs._valign==rtk.Widget.BOTTOM then +y=offy+cellh-wcalc.h-cbp +end +wcalc.x=wcalc.x+x +widget.box[1]=x +wcalc.y=wcalc.y+y +widget.box[2]=y +end +end)() + +__mod_rtk_flowbox=(function() +local rtk=__mod_rtk_core +rtk.FlowBox=rtk.class('rtk.FlowBox', rtk.Container)rtk.FlowBox.register{vspacing=rtk.Attribute{default=0,reflow=rtk.Widget.REFLOW_FULL,},hspacing=rtk.Attribute{default=0,reflow=rtk.Widget.REFLOW_FULL,},}function rtk.FlowBox:initialize(attrs,...)rtk.Container.initialize(self,attrs,self.class.attributes.defaults,...)end +function rtk.FlowBox:_reflow(boxx,boxy,boxw,boxh,fillw,fillh,clampw,clamph,uiscale,viewport,window)local calc=self.calc +local x,y=self:_get_box_pos(boxx,boxy)local w,h,tp,rp,bp,lp=self:_get_content_size(boxw,boxh,fillw,fillh,clampw,clamph)local inner_maxw=w or(boxw-lp-rp)local inner_maxh=h or(boxh-tp-bp)clampw=clampw or w~=nil or fillw +clamph=clamph or h~=nil or fillh +local child_geometry={}local hspacing=(calc.hspacing or 0)*rtk.scale.value +local vspacing=(calc.vspacing or 0)*rtk.scale.value +self._reflowed_children={}self._child_index_by_id={}local child_maxw=0 +local child_totalh=0 +for _,widgetattrs in ipairs(self.children)do +local widget,attrs=table.unpack(widgetattrs)local wcalc=widget.calc +if wcalc.visible==true and wcalc.position&rtk.Widget.POSITION_INFLOW~=0 then +local ctp,crp,cbp,clp=self:_get_cell_padding(widget,attrs)local wx,wy,ww,wh=widget:reflow(0,0,inner_maxw,inner_maxh,nil,nil,clampw,clamph,uiscale,viewport,window +)ww=ww+clp+crp +wh=wh+ctp+cbp +local minw=(attrs.minw or wcalc.minw or 0)+clp+crp +child_maxw=math.min(math.max(child_maxw,ww,minw),inner_maxw)child_totalh=child_totalh+wh +child_geometry[#child_geometry+1]={x=wx,y=wy,w=ww,h=wh}end +end +child_totalh=child_totalh+(#self.children-1)*vspacing +local col_width=child_maxw +local num_columns=math.floor((inner_maxw+hspacing)/(col_width+hspacing))local col_height=h +if not col_height and #child_geometry>0 then +col_height=child_geometry[1].h +for i=2,#child_geometry do +local need_columns=1 +local cur_colh=0 +for j=1,#child_geometry do +local wh=child_geometry[j].h +if cur_colh+wh>col_height then +need_columns=need_columns+1 +cur_colh=0 +end +cur_colh=cur_colh+wh+(j>1 and vspacing or 0)end +if need_columns<=num_columns then +num_columns=need_columns +break +end +col_height=col_height+vspacing+child_geometry[i].h +end +end +local col_width_max=math.floor((inner_maxw-((num_columns-1)*hspacing))/num_columns)local col={w=0,h=0,n=1}local offset={x=0,y=0}local inner={w=0,h=0}local chspacing=(col.ncol_height then +inner.w=inner.w+col.w +offset.x=offset.x+col.w +offset.y=0 +col.w,col.h=0,0 +col.n=col.n+1 +chspacing=(col.ncliph or self.calc.ghost then +return false +end +self:_handle_drawpre(offx,offy,alpha,event)self:_draw_bg(offx,offy,alpha,event)self:_draw_borders(offx,offy,alpha)self:_handle_draw(offx,offy,alpha,event)end +end)() + +__mod_rtk_button=(function() +local rtk=__mod_rtk_core +rtk.Button=rtk.class('rtk.Button', rtk.Widget)rtk.Button.static.RAISED=false +rtk.Button.static.FLAT=true +rtk.Button.static.LABEL=2 +rtk.Button.register{[1]=rtk.Attribute{alias='label'},label=rtk.Attribute{reflow=rtk.Widget.REFLOW_FULL},icon=rtk.Attribute{priority=true,reflow=rtk.Widget.REFLOW_FULL,calculate=function(self,attr,value,target)if type(value)=='string' then +local color=self.color +if self.calc.flat==rtk.Button.FLAT then +color=rtk.theme.bg +end +local style=rtk.color.get_icon_style(color,rtk.theme.bg)if self.icon and self.icon.style==style then +return self.icon +end +local img=rtk.Image.icon(value,style)if not img then +img=rtk.Image.make_placeholder_icon(24,24,style)end +return img +else +return value +end +end,},wrap=rtk.Attribute{default=false,reflow=rtk.Widget.REFLOW_FULL,},color=rtk.Attribute{default=function(self,attr)return rtk.theme.button +end,calculate=function(self,attr,value,target)local color=rtk.Widget.attributes.bg.calculate(self,attr,value,target)local luma=rtk.color.luma(color,rtk.theme.bg)local dark=luma0 and tagw or(calc.w-ix+calc.spacing))end +cliph=calc.h-ly +end +self._pre={tp=tp,rp=rp,bp=bp,lp=lp,ix=ix,iy=iy,lx=lx,ly=ly,lw=lw,lh=lh,tagx=tagx,tagw=tagw,surx=surx,sury=sury,surw=surw or 0,surh=surh or 0,clipw=clipw,cliph=cliph,iw=icon and(icon.w*iscale),ih=icon and(icon.h*iscale),}end +function rtk.Button:_draw(offx,offy,alpha,event,clipw,cliph,cltargetx,cltargety,parentx,parenty)local calc=self.calc +if calc.disabled then +alpha=alpha*0.5 +end +rtk.Widget._draw(self,offx,offy,alpha,event,clipw,cliph,cltargetx,cltargety,parentx,parenty)local x=calc.x+offx +local y=calc.y+offy +if y+calc.h<0 or y>cliph or calc.ghost then +return false +end +local hover=(self.hovering or calc.hover)and not calc.disabled +local clicked=hover and event.buttons~=0 and self:focused()and self.window.is_focused +local theme=self._theme +local gradient,brightness,cmul,bmul +if clicked then +gradient=theme.button_clicked_gradient*theme.button_gradient_mul +brightness=theme.button_clicked_brightness +cmul=theme.button_clicked_mul +bmul=theme.button_clicked_border_mul +elseif hover then +gradient=theme.button_hover_gradient*theme.button_gradient_mul +brightness=theme.button_hover_brightness +cmul=theme.button_hover_mul +bmul=theme.button_hover_border_mul +else +gradient=theme.button_normal_gradient*theme.button_gradient_mul +bmul=theme.button_normal_border_mul +brightness=1.0 +cmul=1.0 +end +self:_handle_drawpre(offx,offy,alpha,event)if self.circular then +self:_draw_circular_button(x,y,hover,clicked,gradient,brightness,cmul,bmul,alpha)else +self:_draw_rectangular_button(x,y,hover,clicked,gradient,brightness,cmul,bmul,alpha)self:_draw_borders(offx,offy,alpha)end +self:_handle_draw(offx,offy,alpha,event)end +function rtk.Button:_is_mouse_over(clparentx,clparenty,event)local calc=self.calc +if calc.circular then +local x=calc.x+clparentx+self._radius +local y=calc.y+clparenty+self._radius +return self.window and self.window.in_window and +rtk.point_in_circle(event.x,event.y,x,y,self._radius)else +return rtk.Widget._is_mouse_over(self,clparentx,clparenty,event)end +end +function rtk.Button:_draw_circular_button(x,y,hover,clicked,gradient,brightness,cmul,bmul,alpha)local calc=self.calc +local radius=math.ceil(self._radius)local cirx=math.floor(x)+radius +local ciry=math.floor(y)+radius +local icon=calc.icon +if calc.surface and(not calc.flat or hover or clicked)then +if calc.elevation>0 then +self._shadow:draw(x+1,y+1)end +local r,g,b,a=rtk.color.mod(calc.color,1.0,1.0,brightness)self:setcolor({r*cmul,g*cmul,b*cmul,a},alpha)gfx.circle(cirx,ciry,radius,1,1)end +if icon then +local ix=(calc.w-(icon.w*rtk.scale.value))/2 +local iy=(calc.h-(icon.h*rtk.scale.value))/2 +self:_draw_icon(x+ix,y+iy,hover,alpha)end +if calc.border then +local color,thickness=table.unpack(calc.border)self:setcolor(color)for i=1,thickness do +gfx.circle(cirx,ciry,radius-(i-1),0,1)end +end +end +function rtk.Button:_draw_rectangular_button(x,y,hover,clicked,gradient,brightness,cmul,bmul,alpha)local calc=self.calc +local pre=self._pre +local amul=calc.alpha*alpha +local label_over_surface=calc.surface and(calc.flat==rtk.Button.RAISED or hover)local textcolor=label_over_surface and calc.textcolor or calc.textcolor2 +local draw_surface=label_over_surface or(calc.label and calc.tagged and calc.surface)local tagx=x+pre.tagx +local surx=x+pre.surx +local sury=y+pre.sury +local surw=pre.surw +local surh=pre.surh +if calc.tagged and calc.flat==rtk.Button.LABEL and calc.surface and not hover then +surx=tagx +surw=pre.tagw +end +if surw>0 and surh>0 and draw_surface then +local d=(gradient*calc.gradient)/calc.h +local lmul=1-calc.h*d/2 +local r,g,b,a=rtk.color.rgba(calc.color)local sr,sg,sb,sa=rtk.color.mod({r,g,b,a},1.0,1.0,brightness*lmul,amul)gfx.gradrect(surx,sury,surw,surh,sr*cmul,sg*cmul,sb*cmul,sa*amul,0,0,0,0,r*d,g*d,b*d,0)gfx.set(r*bmul,g*bmul,b*bmul,amul)gfx.rect(surx,sury,surw,surh,0)if pre.tagw>0 and(hover or calc.flat~=rtk.Button.LABEL)then +local ta=1-(calc.tagalpha or self._theme.button_tag_alpha)self:setcolor({0,0,0,1})gfx.muladdrect(tagx,sury,pre.tagw,surh,ta,ta,ta,1.0)end +elseif calc.bg then +self:setcolor(calc.bg)gfx.rect(x,y,calc.w,calc.h,1)end +if calc.icon then +self:_draw_icon(x+pre.ix,y+pre.iy,hover,alpha)end +if calc.label then +self:setcolor(textcolor,alpha)self._font:draw(self._segments,x+pre.lx,y+pre.ly,pre.clipw,pre.cliph)end +end +function rtk.Button:_draw_icon(x,y,hovering,alpha)self.calc.icon:draw(x,y,self.calc.alpha*alpha,rtk.scale.value)end +end)() + +__mod_rtk_entry=(function() +local rtk=__mod_rtk_core +local log=__mod_rtk_log +rtk.Entry=rtk.class('rtk.Entry', rtk.Widget)rtk.Entry.static.contextmenu={{'Undo', id='undo'},rtk.NativeMenu.SEPARATOR,{'Cut', id='cut'},{'Copy', id='copy'},{'Paste', id='paste'},{'Delete', id='delete'},rtk.NativeMenu.SEPARATOR,{'Select All', id='select_all'},}rtk.Entry.register{[1]=rtk.Attribute{alias='value'},value=rtk.Attribute{default='',reflow=rtk.Widget.REFLOW_NONE,calculate=function(self,attr,value,target)return value and tostring(value) or ''end,},textwidth=rtk.Attribute{reflow=rtk.Widget.REFLOW_FULL},icon=rtk.Attribute{reflow=rtk.Widget.REFLOW_FULL,calculate=function(self,attr,value,target)if type(value)=='string' then +local icon=self.calc.icon +local style=rtk.color.get_icon_style(self.calc.bg or rtk.theme.bg,rtk.theme.bg)if icon and icon.style==style then +return icon +end +local img=rtk.Image.icon(value,style)if not img then +img=rtk.Image.make_placeholder_icon(24,24,style)end +return img +else +return value +end +end +},icon_alpha=0.6,spacing=rtk.Attribute{default=5,reflow=rtk.Widget.REFLOW_FULL +},placeholder=rtk.Attribute{default=nil,reflow=rtk.Widget.REFLOW_FULL,},textcolor=rtk.Attribute{default=function(self,attr)return rtk.theme.text +end,calculate=rtk.Reference('bg')},border_hover=rtk.Attribute{default=function(self,attr)return {rtk.theme.entry_border_hover,1}end,reflow=rtk.Widget.REFLOW_FULL,calculate=function(self,attr,value,target)return rtk.Widget.static._calc_border(self,value)end,},border_focused=rtk.Attribute{default=function(self,attr)return {rtk.theme.entry_border_focused,1}end,reflow=rtk.Widget.REFLOW_FULL,calculate=rtk.Reference('border_hover'),},blink=true,caret=rtk.Attribute{type='number',default=1,priority=true,reflow=rtk.Widget.REFLOW_NONE,calculate=function(self,attr,value,target)return rtk.clamp(value, 1, #(target.value or '') + 1)end,},font=rtk.Attribute{reflow=rtk.Widget.REFLOW_FULL,default=function(self,attr)return self._theme_font[1] +end +},fontsize=rtk.Attribute{reflow=rtk.Widget.REFLOW_FULL,default=function(self,attr)return self._theme_font[2] +end +},fontscale=rtk.Attribute{default=1.0,reflow=rtk.Widget.REFLOW_FULL +},fontflags=rtk.Attribute{default=function(self,attr)return self._theme_font[3] +end +},bg=rtk.Attribute{default=function(self,attr)return rtk.theme.entry_bg +end +},tpadding=4,rpadding=10,bpadding=4,lpadding=10,cursor=rtk.mouse.cursors.BEAM,autofocus=true,}function rtk.Entry:initialize(attrs,...)self._theme_font=rtk.theme.entry_font or rtk.theme.default_font +rtk.Widget.initialize(self,attrs,self.class.attributes.defaults,...)self._positions={0}self._backingstore=nil +self._font=rtk.Font()self._caretctr=0 +self._selstart=nil +self._selend=nil +self._loffset=0 +self._blinking=false +self._dirty_text=false +self._dirty_positions=nil +self._dirty_view=false +self._history=nil +self._last_doubleclick_time=0 +self._num_doubleclicks=0 +end +function rtk.Entry:_handle_attr(attr,value,oldval,trigger,reflow,sync)local calc=self.calc +local ok=rtk.Widget._handle_attr(self,attr,value,oldval,trigger,reflow,sync)if ok==false then +return ok +end +if attr=='value' then +self._dirty_text=true +if not self._dirty_positions then +local diff=math.min(#value,#oldval)for i=1,diff do +if value:sub(i,i)~=oldval:sub(i,i)then +diff=i +break +end +end +self._dirty_positions=diff +end +self._selstart=nil +local caret=rtk.clamp(calc.caret,1,#value+1)if caret~=calc.caret then +self:sync('caret', caret)end +if trigger then +self:_handle_change()end +elseif attr=='caret' then +self._dirty_view=true +elseif attr == 'bg' and type(self.icon) == 'string' then +self:attr('icon', self.icon, true)elseif attr=='icon' and value then +self._last_reflow_scale=nil +end +return true +end +function rtk.Entry:_reflow(boxx,boxy,boxw,boxh,fillw,fillh,clampw,clamph,uiscale,viewport,window)local calc=self.calc +local maxw,maxh=nil,nil +if self._font:set(calc.font,calc.fontsize,calc.fontscale,calc.fontflags)then +self._dirty_positions=1 +end +if calc.icon and uiscale~=self._last_reflow_scale then +calc.icon:refresh_scale()self._last_reflow_scale=uiscale +end +if calc.textwidth and not self.w then +local charwidth, _=gfx.measurestr('W')maxw,maxh=charwidth*calc.textwidth,self._font.texth +else +maxw, maxh=gfx.measurestr(calc.placeholder or "Dummy string!")end +calc.x,calc.y=self:_get_box_pos(boxx,boxy)local w,h,tp,rp,bp,lp=self:_get_content_size(boxw,boxh,fillw,fillh,clampw,clamph)calc.w=(w or maxw)+lp+rp +calc.h=(h or maxh)+tp+bp +self._ctp,self._crp,self._cbp,self._clp=tp,rp,bp,lp +if not self._backingstore then +self._backingstore=rtk.Image()end +self._backingstore:resize(calc.w,calc.h,false)self._dirty_text=true +end +function rtk.Entry:_unrealize()rtk.Widget._unrealize(self)self._backingstore=nil +end +function rtk.Entry:_calcpositions(startfrom)startfrom=startfrom or 1 +local value=self.calc.value +self._font:set()for i=startfrom,#value+1 do +local w,_=gfx.measurestr(value:sub(1,i))self._positions[i+1]=w +end +self._dirty_positions=nil +end +function rtk.Entry:_calcview()local calc=self.calc +local curx=self._positions[calc.caret] +local curoffset=curx-self._loffset +local innerw=calc.w-(self._clp+self._crp)if calc.icon then +innerw=innerw-(calc.icon.w*rtk.scale.value/calc.icon.density)-calc.spacing +end +local loffset=self._loffset +if curoffset<0 then +loffset=curx +elseif curoffset>innerw then +loffset=curx-innerw +end +local last=self._positions[#calc.value+1] +if last>innerw then +local gap=innerw-(last-loffset)if gap>0 then +loffset=loffset-gap +end +else +loffset=0 +end +if loffset~=self._loffset then +self._dirty_text=true +self._loffset=loffset +end +self._dirty_view=false +end +function rtk.Entry:_handle_focus(event,context)local ok=rtk.Widget._handle_focus(self,event,context)self._dirty_text=self._dirty_text or(ok and self._selstart)return ok +end +function rtk.Entry:_handle_blur(event,other)local ok=rtk.Widget._handle_blur(self,event,other)self._dirty_text=self._dirty_text or(ok and self._selstart)return ok +end +function rtk.Entry:_blink()if self.calc.blink and self:focused()then +self._blinking=true +local ctr=self._caretctr%16 +self._caretctr=self._caretctr+1 +if ctr==0 then +self:queue_draw()end +rtk.defer(self._blink,self)end +end +function rtk.Entry:_caret_from_mouse_event(event)local calc=self.calc +local iconw=calc.icon and(calc.icon.w*rtk.scale.value/calc.icon.density+calc.spacing)or 0 +local relx=self._loffset+event.x-self.clientx-iconw-self._clp +for i=2,calc.value:len()+1 do +local pos=self._positions[i] +local width=pos-self._positions[i-1] +if relx<=self._positions[i]-width/2 then +return i-1 +end +end +return calc.value:len()+1 +end +local function is_word_break_character(value,pos)local c=value:sub(pos,pos)return c ~='_' and c:match('[%c%p%s]')end +function rtk.Entry:_get_word_left(spaces)local value=self.calc.value +local caret=self.calc.caret +if spaces then +while caret>1 and is_word_break_character(value,caret-1)do +caret=caret-1 +end +end +while caret>1 and not is_word_break_character(value,caret-1)do +caret=caret-1 +end +return caret +end +function rtk.Entry:_get_word_right(spaces)local value=self.calc.value +local caret=self.calc.caret +local len=value:len()while caret<=len and not is_word_break_character(value,caret)do +caret=caret+1 +end +if spaces then +while caret<=len and is_word_break_character(value,caret)do +caret=caret+1 +end +end +return caret +end +function rtk.Entry:select_all()self._selstart=1 +self._selend=self.calc.value:len()+1 +self._dirty_text=true +self:queue_draw()end +function rtk.Entry:select_range(a,b)local len=#self.calc.value +if len==0 or not a then +self._selstart=nil +else +b=b or a +self._selstart=math.max(1,a)self._selend=b>0 and math.min(len+1,b+1)or math.max(self._selstart,len+b+2)end +self._dirty_text=true +self:queue_draw()end +function rtk.Entry:get_selection_range()if self._selstart then +return math.min(self._selstart,self._selend),math.max(self._selstart,self._selend)end +end +function rtk.Entry:_edit(insert,delete_selection,dela,delb,caret)local calc=self.calc +local value=calc.value +if delete_selection then +dela,delb=self:get_selection_range()if dela and delb then +local ndeleted=delb-dela +caret=rtk.clamp(dela,1,#value)end +end +caret=caret or calc.caret +if dela and delb then +dela=rtk.clamp(dela,1,#value)delb=rtk.clamp(delb,1,#value+1)value=value:sub(1,dela-1)..value:sub(delb)self._dirty_positions=math.min(dela-1,self._dirty_positions or math.inf)end +if insert then +self._dirty_positions=math.min(caret-1,self._dirty_positions or math.inf)value=value:sub(0,caret-1)..insert..value:sub(caret)caret=caret+insert:len()end +if value~=calc.value then +caret=rtk.clamp(caret,1,#value+1)self:sync('value', value)if caret~=calc.caret then +self:sync('caret', caret)end +self._dirty_view=true +end +end +function rtk.Entry:delete_range(a,b)self:push_undo()self:_edit(nil,nil,a,b)end +function rtk.Entry:delete()if self._selstart then +self:push_undo()end +self:_edit(nil,true)end +function rtk.Entry:clear()if self.calc.value ~='' then +self:push_undo()self:sync('value', '')end +end +function rtk.Entry:copy()if self._selstart then +local a,b=self:get_selection_range()local text=self.calc.value:sub(a,b-1)if rtk.clipboard.set(text)then +return text +end +end +end +function rtk.Entry:cut()local copied=self:copy()if copied then +self:delete()end +return copied +end +function rtk.Entry:paste()local str=rtk.clipboard.get()if str and str ~='' then +self:push_undo()self:_edit(str,true)return str +end +end +function rtk.Entry:insert(text)self:push_undo()self:_edit(text)end +function rtk.Entry:undo()local calc=self.calc +if self._history and #self._history>0 then +local state=table.remove(self._history,#self._history)local value,caret +value,caret,self._selstart,self._selend=table.unpack(state)self:sync('value', value)self:sync('caret', caret)return true +else +return false +end +end +function rtk.Entry:push_undo()if not self._history then +self._history={}end +local calc=self.calc +self._history[#self._history+1]={calc.value,calc.caret,self._selstart,self._selend}end +function rtk.Entry:_handle_mousedown(event)local ok=rtk.Widget._handle_mousedown(self,event)if ok==false then +return ok +end +if event.button==rtk.mouse.BUTTON_LEFT then +local caret=self:_caret_from_mouse_event(event)self._selstart=nil +self._dirty_text=true +self:sync('caret', caret)self:queue_draw()elseif event.button==rtk.mouse.BUTTON_RIGHT then +if not self._popup then +self._popup=rtk.NativeMenu(rtk.Entry.contextmenu)end +local clipboard=rtk.clipboard.get()self._popup:item('undo').disabled = not self._history or #self._history == 0 +self._popup:item('cut').disabled = not self._selstart +self._popup:item('copy').disabled = not self._selstart +self._popup:item('delete').disabled = not self._selstart +self._popup:item('paste').disabled = not clipboard or clipboard == ''self._popup:item('select_all').disabled = #self.calc.value == 0 +self._popup:open_at_mouse():done(function(item)if item then +self[item.id](self)end +end)end +return true +end +function rtk.Entry:_handle_keypress(event)local ok=rtk.Widget._handle_keypress(self,event)if ok==false then +return ok +end +local calc=self.calc +local newcaret=nil +local len=calc.value:len()local orig_caret=calc.caret +local selecting=event.shift +if event.keycode==rtk.keycodes.LEFT then +if not selecting and self._selstart then +newcaret=self._selstart +elseif event.ctrl then +newcaret=self:_get_word_left(true)else +newcaret=math.max(1,calc.caret-1)end +elseif event.keycode==rtk.keycodes.RIGHT then +if not selecting and self._selstart then +newcaret=self._selend +elseif event.ctrl then +newcaret=self:_get_word_right(true)else +newcaret=math.min(calc.caret+1,len+1)end +elseif event.keycode==rtk.keycodes.HOME then +newcaret=1 +elseif event.keycode==rtk.keycodes.END then +newcaret=calc.value:len()+1 +elseif event.keycode==rtk.keycodes.DELETE then +if self._selstart then +self:delete()else +if event.ctrl then +self:push_undo()self:_edit(nil,false,calc.caret,self:_get_word_right(true)-1)elseif calc.caret<=len then +self:_edit(nil,false,calc.caret,calc.caret+1)end +end +elseif event.keycode==rtk.keycodes.BACKSPACE then +if calc.caret>=1 then +if self._selstart then +self:delete()else +if event.ctrl then +self:push_undo()local caret=self:_get_word_left(true)self:_edit(nil,false,caret,calc.caret,caret)elseif calc.caret>1 then +local caret=calc.caret-1 +self:_edit(nil,false,caret,caret+1,caret)end +end +end +elseif event.char and not event.ctrl then +if self._selstart then +self:push_undo()end +self:_edit(event.char,true)selecting=false +elseif event.ctrl and event.char and not event.shift then +if event.char=='a' and len > 0 then +self:select_all()selecting=nil +elseif event.char=='c' then +self:copy()return true +elseif event.char=='x' then +self:cut()elseif event.char=='v' then +self:paste()elseif event.char=='z' then +self:undo()selecting=nil +end +else +return ok +end +if newcaret then +self:sync('caret', newcaret)end +if selecting then +if not self._selstart then +self._selstart=orig_caret +end +self._selend=calc.caret +self._dirty_text=true +elseif selecting==false and self._selstart then +self._selstart=nil +self._dirty_text=true +end +self._caretctr=0 +log.debug2('keycode=%s char=%s caret=%s ctrl=%s shift=%s meta=%s alt=%s sel=%s-%s',event.keycode,event.char,calc.caret,event.ctrl,event.shift,event.meta,event.alt,self._selstart,self._selend +)return true +end +function rtk.Entry:_get_touch_activate_delay(event)if self:focused()then +return 0 +else +return rtk.Widget._get_touch_activate_delay(self,event)end +end +function rtk.Entry:_handle_dragstart(event)if not self:focused()or event.button~=rtk.mouse.BUTTON_LEFT then +return +end +local draggable,droppable=self:ondragstart(self,event)if draggable==nil then +self._selstart=self.calc.caret +self._selend=self.calc.caret +return true,false +end +return draggable,droppable +end +function rtk.Entry:_handle_dragmousemove(event)local ok=rtk.Widget._handle_dragmousemove(self,event)if ok==false then +return ok +end +local selend=self:_caret_from_mouse_event(event)if selend==self._selend then +return ok +end +self._selend=selend +self:sync('caret', selend)self._dirty_text=true +return ok +end +function rtk.Entry:_handle_click(event)local ok=rtk.Widget._handle_click(self,event)if ok==false or event.button~=rtk.mouse.BUTTON_LEFT then +return ok +end +if event.time-self._last_doubleclick_time<0.7 then +self:select_all()self._last_doubleclick_time=0 +elseif rtk.dnd.dragging~=self then +self:select_range(nil)rtk.Widget.focus(self)end +return ok +end +function rtk.Entry:_handle_doubleclick(event)local ok=rtk.Widget._handle_doubleclick(self,event)if ok==false or event.button~=rtk.mouse.BUTTON_LEFT then +return ok +end +self._last_doubleclick_time=event.time +local left=self:_get_word_left(false)local right=self:_get_word_right(true)self:sync('caret', right)self:select_range(left,right-1)return true +end +function rtk.Entry:_rendertext(x,y)self._font:set()self._backingstore:blit{src=gfx.dest,sx=x+self._clp,sy=y+self._ctp,mode=rtk.Image.FAST_BLIT +}self._backingstore:pushdest()if self._selstart and self:focused()then +local a,b=self:get_selection_range()self:setcolor(rtk.theme.entry_selection_bg)gfx.rect(self._positions[a]-self._loffset,0,self._positions[b]-self._positions[a],self._backingstore.h,1 +)end +self:setcolor(self.calc.textcolor)self._font:draw(self.calc.value,-self._loffset,rtk.os.mac and 1 or 0)self._backingstore:popdest()self._dirty_text=false +end +function rtk.Entry:_draw(offx,offy,alpha,event,clipw,cliph,cltargetx,cltargety,parentx,parenty)local calc=self.calc +if offy~=self.offy or offx~=self.offx then +self._dirty_text=true +end +rtk.Widget._draw(self,offx,offy,alpha,event,clipw,cliph,cltargetx,cltargety,parentx,parenty)local x,y=calc.x+offx,calc.y+offy +local focused=self:focused()if(y+calc.h<0 or y>cliph or calc.ghost)and not focused then +return false +end +if self.disabled then +alpha=alpha*0.5 +end +local scale=rtk.scale.value +local tp,rp,bp,lp=self._ctp,self._crp,self._cbp,self._clp +self:_handle_drawpre(offx,offy,alpha,event)self:_draw_bg(offx,offy,alpha,event)if self._dirty_positions then +self:_calcpositions(self._dirty_positions)end +if self._dirty_view or self._dirty_text then +self:_calcview()end +if self._dirty_text then +self:_rendertext(x,y)end +local amul=calc.alpha*alpha +local icon=calc.icon +if icon then +local a=math.min(1,calc.icon_alpha*alpha+(focused and 0.2 or 0))icon:draw(x+lp,y+((calc.h+tp-bp)-icon.h*scale/icon.density)/2,a*amul,scale +)lp=lp+icon.w*scale/icon.density+calc.spacing +end +self._backingstore:blit{sx=0,sy=0,sw=calc.w-lp-rp,sh=calc.h-tp-bp,dx=x+lp,dy=y+tp,alpha=amul,mode=rtk.Image.FAST_BLIT +}if calc.placeholder and #calc.value==0 then +self._font:set()self:setcolor(rtk.theme.entry_placeholder,alpha)self._font:draw(calc.placeholder,x+lp,y+tp+(rtk.os.mac and 1 or 0),calc.w-lp,calc.h-tp +)end +if focused then +local showcursor=not self._selstart or(self._selend-self._selstart)==0 +if not self._blinking and showcursor then +self:_blink()end +self:_draw_borders(offx,offy,alpha,calc.border_focused)if self._caretctr%32<16 and showcursor then +local curx=x+self._positions[calc.caret]+lp-self._loffset +self:setcolor(calc.textcolor,alpha)gfx.line(curx,y+tp,curx,y+calc.h-bp,0)end +else +self._blinking=false +if self.hovering then +self:_draw_borders(offx,offy,alpha,calc.border_hover)else +self:_draw_borders(offx,offy,alpha)end +end +self:_handle_draw(offx,offy,alpha,event)end +function rtk.Entry:onchange(event)end +function rtk.Entry:_handle_change(event)return self:onchange(event)end +end)() + +__mod_rtk_text=(function() +local rtk=__mod_rtk_core +rtk.Text=rtk.class('rtk.Text', rtk.Widget)rtk.Text.static.WRAP_NONE=false +rtk.Text.static.WRAP_NORMAL=true +rtk.Text.static.WRAP_BREAK_WORD=2 +rtk.Text.register{[1]=rtk.Attribute{alias='text'},text=rtk.Attribute{default='Text',reflow=rtk.Widget.REFLOW_FULL,},color=rtk.Attribute{default=function(self,attr)return rtk.theme.text +end,calculate=rtk.Reference('bg'),},wrap=rtk.Attribute{default=rtk.Text.WRAP_NONE,reflow=rtk.Widget.REFLOW_FULL,calculate={['none']=rtk.Text.WRAP_NONE,['normal']=rtk.Text.WRAP_NORMAL,['break-word']=rtk.Text.WRAP_BREAK_WORD +},},textalign=rtk.Attribute{default=nil,calculate=rtk.Reference('halign'),},overflow=false,spacing=rtk.Attribute{default=0,reflow=rtk.Widget.REFLOW_FULL,},font=rtk.Attribute{default=function(self,attr)return self._theme_font[1] +end,reflow=rtk.Widget.REFLOW_FULL,},fontsize=rtk.Attribute{default=function(self,attr)return self._theme_font[2] +end,reflow=rtk.Widget.REFLOW_FULL,},fontscale=rtk.Attribute{default=1.0,reflow=rtk.Widget.REFLOW_FULL,},fontflags=rtk.Attribute{default=function(self,attr)return self._theme_font[3] +end +},}function rtk.Text:initialize(attrs,...)self._theme_font=self._theme_font or rtk.theme.text_font or rtk.theme.default_font +rtk.Widget.initialize(self,attrs,rtk.Text.attributes.defaults,...)self._font=rtk.Font()end +function rtk.Text:__tostring_info()return self.text +end +function rtk.Text:_handle_attr(attr,value,oldval,trigger,reflow,sync)if attr == 'text' and reflow == rtk.Widget.REFLOW_DEFAULT and self.w and not self.calc.wrap then +if not value:find('\n') and not oldval:find('\n') then +reflow=rtk.Widget.REFLOW_PARTIAL +end +end +local ok=rtk.Widget._handle_attr(self,attr,value,oldval,trigger,reflow,sync)if ok==false then +return ok +end +if self._segments and (attr == 'text' or attr == 'wrap' or attr == 'textalign' or attr == 'spacing') then +self._segments.dirty=true +end +return ok +end +function rtk.Text:_reflow(boxx,boxy,boxw,boxh,fillw,fillh,clampw,clamph,uiscale,viewport,window)local calc=self.calc +calc.x,calc.y=self:_get_box_pos(boxx,boxy)self._font:set(calc.font,calc.fontsize,calc.fontscale,calc.fontflags)local w,h,tp,rp,bp,lp=self:_get_content_size(boxw,boxh,fillw,fillh,clampw,clamph)local hpadding=lp+rp +local vpadding=tp+bp +local lmaxw=(clampw or fillw)and(boxw-hpadding)or w or math.inf +local lmaxh=(clamph or fillh)and(boxh-vpadding)or h or math.inf +local seg=self._segments +if not seg or seg.boxw~=lmaxw or not seg.isvalid()then +self._segments,self.lw,self.lh=self._font:layout(calc.text,lmaxw,lmaxh,calc.wrap~=rtk.Text.WRAP_NONE,self.textalign and calc.textalign or calc.halign,true,calc.spacing,calc.wrap==rtk.Text.WRAP_BREAK_WORD +)end +calc.w=(w and w+hpadding)or(fillw and boxw)or math.min(clampw and boxw or math.inf,self.lw+hpadding)calc.h=(h and h+vpadding)or(fillh and boxh)or math.min(clamph and boxh or math.inf,self.lh+vpadding)calc.w=math.round(rtk.clamp(calc.w,calc.minw,calc.maxw))calc.h=math.round(rtk.clamp(calc.h,calc.minh,calc.maxh))end +function rtk.Text:_realize_geometry()local calc=self.calc +local tp,rp,bp,lp=self:_get_padding_and_border()local lx,ly +if calc.halign==rtk.Widget.LEFT then +lx=lp +elseif calc.halign==rtk.Widget.CENTER then +lx=lp+math.max(0,calc.w-self.lw-lp-rp)/2 +elseif calc.halign==rtk.Widget.RIGHT then +lx=math.max(0,calc.w-self.lw-rp)end +if calc.valign==rtk.Widget.TOP then +ly=tp +elseif calc.valign==rtk.Widget.CENTER then +ly=tp+math.max(0,calc.h-self.lh-tp-bp)/2 +elseif calc.valign==rtk.Widget.BOTTOM then +ly=math.max(0,calc.h-self.lh-bp)end +self._pre={tp=tp,rp=rp,bp=bp,lp=lp,lx=lx,ly=ly,}end +function rtk.Text:_draw(offx,offy,alpha,event,clipw,cliph,cltargetx,cltargety,parentx,parenty)rtk.Widget._draw(self,offx,offy,alpha,event,clipw,cliph,cltargetx,cltargety,parentx,parenty)local calc=self.calc +local x,y=calc.x+offx,calc.y+offy +if y+calc.h<0 or y>cliph or calc.ghost then +return +end +local pre=self._pre +self:_handle_drawpre(offx,offy,alpha,event)self:_draw_bg(offx,offy,alpha,event)self:setcolor(calc.color,alpha)assert(self._segments)self._font:draw(self._segments,x+pre.lx,y+pre.ly,not calc.overflow and math.min(clipw-x,calc.w)-pre.lx-pre.rp or nil,not calc.overflow and math.min(cliph-y,calc.h)-pre.ly-pre.bp or nil +)self:_draw_borders(offx,offy,alpha)self:_handle_draw(offx,offy,alpha,event)end +end)() + +__mod_rtk_heading=(function() +local rtk=__mod_rtk_core +rtk.Heading=rtk.class('rtk.Heading', rtk.Text)rtk.Heading.register{color=rtk.Attribute{default=function(self,attr)return rtk.theme.heading or rtk.theme.text +end +},}function rtk.Heading:initialize(attrs,...)self._theme_font=self._theme_font or rtk.theme.heading_font or rtk.theme.default_font +rtk.Text.initialize(self,attrs,self.class.attributes.defaults,...)end +end)() + +__mod_rtk_imagebox=(function() +local rtk=__mod_rtk_core +local log=__mod_rtk_log +rtk.ImageBox=rtk.class('rtk.ImageBox', rtk.Widget)rtk.ImageBox.register{[1]=rtk.Attribute{alias='image'},image=rtk.Attribute{calculate=rtk.Entry.attributes.icon.calculate,reflow=rtk.Widget.REFLOW_FULL,},scale=rtk.Attribute{reflow=rtk.Widget.REFLOW_FULL,},aspect=rtk.Attribute{reflow=rtk.Widget.REFLOW_FULL,},}function rtk.ImageBox:initialize(attrs,...)rtk.Widget.initialize(self,attrs,self.class.attributes.defaults,...)end +function rtk.ImageBox:_handle_attr(attr,value,oldval,trigger,reflow,sync)local ret=rtk.Widget._handle_attr(self,attr,value,oldval,trigger,reflow,sync)if ret==false then +return ret +end +if attr=='image' and value then +self._last_reflow_scale=nil +end +return ret +end +function rtk.ImageBox:_reflow(boxx,boxy,boxw,boxh,fillw,fillh,clampw,clamph,uiscale,viewport,window)local calc=self.calc +calc.x,calc.y=self:_get_box_pos(boxx,boxy)local w,h,tp,rp,bp,lp=self:_get_content_size(boxw,boxh,fillw,fillh,clampw,clamph,self.scale or 1 +)local dstw,dsth=0,0 +local hpadding=lp+rp +local vpadding=tp+bp +local image=calc.image +if image then +if uiscale~=self._last_reflow_scale then +image:refresh_scale()self._last_reflow_scale=uiscale +end +local scale=(self.scale or 1)*rtk.scale.value/image.density +local native_aspect=image.w/image.h +local aspect=calc.aspect or native_aspect +dstw=w or(fillw and(boxw-hpadding))dsth=h or(fillh and(boxh-vpadding))local constrain=self.scale==nil and not w and not h +if dstw and not dsth then +dsth=math.min(clamph and boxw or math.inf,dstw)/aspect +elseif not dstw and dsth then +dstw=math.min(clampw and boxh or math.inf,dsth)*aspect +elseif not dstw and not dsth then +dstw=image.w*scale/(native_aspect/aspect)dsth=image.h*scale +end +if constrain then +if dstw+hpadding>boxw then +dstw=boxw-hpadding +dsth=dstw/aspect +end +if dsth+vpadding>boxh then +dsth=boxh-vpadding +dstw=dsth*aspect +end +end +self.iscale=dstw/image.w +calc.aspect=aspect +calc.scale=self.iscale +else +self.iscale=1.0 +end +self.iw=math.round(math.max(0,dstw))self.ih=math.round(math.max(0,dsth))calc.w=(fillw and boxw)or math.min(clampw and boxw or math.inf,self.iw+hpadding)calc.h=(fillh and boxh)or math.min(clamph and boxh or math.inf,self.ih+vpadding)calc.w=math.round(rtk.clamp(calc.w,self.minw,self.maxw))calc.h=math.round(rtk.clamp(calc.h,self.minh,self.maxh))end +function rtk.ImageBox:_realize_geometry()local calc=self.calc +local tp,rp,bp,lp=self:_get_padding_and_border()local ix,iy +if calc.halign==rtk.Widget.LEFT then +ix=lp +elseif calc.halign==rtk.Widget.CENTER then +ix=lp+math.max(0,calc.w-self.iw-lp-rp)/2 +elseif calc.halign==rtk.Widget.RIGHT then +ix=math.max(0,calc.w-self.iw-rp)end +if calc.valign==rtk.Widget.TOP then +iy=tp +elseif calc.valign==rtk.Widget.CENTER then +iy=tp+math.max(0,calc.h-self.ih-tp-bp)/2 +elseif calc.valign==rtk.Widget.BOTTOM then +iy=math.max(0,calc.h-self.ih-bp)end +self._pre={ix=ix,iy=iy}end +function rtk.ImageBox:_draw(offx,offy,alpha,event,clipw,cliph,cltargetx,cltargety,parentx,parenty)rtk.Widget._draw(self,offx,offy,alpha,event,clipw,cliph,cltargetx,cltargety,parentx,parenty)local calc=self.calc +local x,y=calc.x+offx,calc.y+offy +if not self.image or y+calc.h<0 or y>cliph or calc.ghost then +return +end +local pre=self._pre +self:_handle_drawpre(offx,offy,alpha,event)self:_draw_bg(offx,offy,alpha,event)calc.image:blit{dx=x+pre.ix,dy=y+pre.iy,dw=self.iw,dh=self.ih,alpha=calc.alpha*alpha,clipw=calc.w,cliph=calc.h,}self:_draw_borders(offx,offy,alpha)self:_handle_draw(offx,offy,alpha,event)end +end)() + +__mod_rtk_optionmenu=(function() +local rtk=__mod_rtk_core +rtk.OptionMenu=rtk.class('rtk.OptionMenu', rtk.Button)rtk.OptionMenu.static._icon=nil +rtk.OptionMenu.register{[1]=rtk.Attribute{alias='menu'},menu=nil,icononly=rtk.Attribute{default=false,reflow=rtk.Widget.REFLOW_FULL,},selected=nil,selected_index=nil,selected_id=nil,selected_item=nil,icon=rtk.Attribute{default=function(self)return rtk.OptionMenu.static._icon +end,},iconpos=rtk.Widget.RIGHT,tagged=true,lpadding=10,rpadding=rtk.Attribute{default=function(self)return(self.icononly or self.circular)and self.lpadding or 7 +end +},tagalpha=0.15,}function rtk.OptionMenu:initialize(attrs,...)if not rtk.OptionMenu._icon then +local icon=rtk.Image(13,17)icon:pushdest(icon.id)rtk.color.set(rtk.theme.text)gfx.triangle(2,6,10,6,6,10)icon:popdest()rtk.OptionMenu.static._icon=icon +end +rtk.Button.initialize(self,attrs,self.class.attributes.defaults,...)self._menu=rtk.NativeMenu()self:_handle_attr('menu', self.calc.menu)self:_handle_attr('icononly', self.calc.icononly)end +function rtk.OptionMenu:_reflow_get_max_label_size(boxw,boxh)local segments,lw,lh=rtk.Button._reflow_get_max_label_size(self,boxw,boxh)local w,h=0,0 +for item in self._menu:items()do +local item_w,item_h=gfx.measurestr(item.altlabel or item.label)w=math.max(w,item_w)h=math.max(h,item_h)end +return segments,rtk.clamp(w,lw,boxw),rtk.clamp(h,lh,boxh)end +function rtk.OptionMenu:select(value,trigger)return self:attr('selected', value, trigger)end +function rtk.OptionMenu:_handle_attr(attr,value,oldval,trigger,reflow,sync)local ok=rtk.Button._handle_attr(self,attr,value,oldval,trigger,reflow,sync)if ok==false then +return ok +end +if attr=='menu' then +self._menu:set(value)if not self.calc.icononly and not self.selected then +self:sync('label', '')elseif self.selected then +self:_handle_attr('selected', self.selected, self.selected, true)end +elseif attr=='selected' then +local item=self._menu:item(value)self.selected_item=item +if item then +if not self.calc.icononly then +self:sync('label', item.altlabel or item.label)end +self.selected_index=item.index +self.selected_id=item.id +rtk.Button.onattr(self,attr,value,oldval,trigger)else +self.selected_index=nil +self.selected_id=nil +if not self.calc.icononly then +self:sync('label', '')end +end +local last=self._menu:item(oldval)if value~=oldval and trigger~=false then +self:onchange(item,last)self:onselect(item,last)elseif trigger then +self:onselect(item,last)end +end +return true +end +function rtk.OptionMenu:open()assert(self.menu, 'menu attribute was not set on OptionMenu')self._menu:open_at_widget(self):done(function(item)if item then +self:sync('selected', item.id or item.index, nil, true)end +end)end +function rtk.OptionMenu:_handle_mousedown(event)local ok=rtk.Button._handle_mousedown(self,event)if ok==false then +return ok +end +self:open()return true +end +function rtk.OptionMenu:onchange(item,lastitem)end +function rtk.OptionMenu:onselect(item,lastitem)end +end)() + +__mod_rtk_checkbox=(function() +local rtk=__mod_rtk_core +rtk.CheckBox=rtk.class('rtk.CheckBox', rtk.Button)rtk.CheckBox.static._icon_unchecked=nil +rtk.CheckBox.static.DUALSTATE=0 +rtk.CheckBox.static.TRISTATE=1 +rtk.CheckBox.static.UNCHECKED=false +rtk.CheckBox.static.CHECKED=true +rtk.CheckBox.static.INDETERMINATE=2 +function rtk.CheckBox.static._make_icons()local w,h=18,18 +local wp,hp=2,2 +local colors +if rtk.theme.dark then +colors={border={1,1,1,0.90},fill={1,1,1,1},check={0,0,0,1},checkaa={0.4,0.4,0.4,1},iborder={1,1,1,0.92},}else +colors={border={0,0,0,0.90},fill={0,0,0,1},check={1,1,1,1},checkaa={0.6,0.6,0.6,1},iborder={0,0,0,0.92},}end +local icon=rtk.Image(w,h)icon:pushdest()rtk.color.set(colors.border)rtk.gfx.roundrect(wp,hp,w-wp*2,h-hp*2,2,1)gfx.rect(wp+1,hp+1,w-wp*2-2,h-hp*2-2,0)icon:popdest()rtk.CheckBox.static._icon_unchecked=icon +icon=rtk.Image(w,h)icon:pushdest()rtk.color.set(colors.fill)rtk.gfx.roundrect(wp,hp,w-wp*2,h-hp*2,2,1)rtk.color.set(colors.fill)gfx.rect(wp+1,hp+1,w-wp*2-2,h-hp*2-2,1)rtk.color.set(colors.checkaa)gfx.x=wp+3 +gfx.y=hp+6 +gfx.lineto(wp+5,hp+9)gfx.lineto(wp+10,hp+3)rtk.color.set(colors.check)gfx.x=wp+2 +gfx.y=hp+6 +gfx.lineto(wp+5,hp+10)gfx.lineto(wp+11,hp+3)icon:popdest()rtk.CheckBox.static._icon_checked=icon +icon=rtk.CheckBox.static._icon_unchecked:clone()icon:pushdest()rtk.color.set(colors.iborder)gfx.rect(wp+3,hp+3,w-wp*2-6,h-hp*2-6)rtk.color.set(colors.fill)gfx.rect(wp+4,hp+4,w-wp*2-8,h-hp*2-8,1)icon:popdest()rtk.CheckBox.static._icon_intermediate=icon +rtk.CheckBox.static._icon_hover=rtk.CheckBox.static._icon_unchecked:clone():recolor(rtk.theme.accent)end +rtk.CheckBox.register{type=rtk.Attribute{default=rtk.CheckBox.DUALSTATE,calculate={dualstate=rtk.CheckBox.DUALSTATE,tristate=rtk.CheckBox.TRISTATE +},},label=nil,value=rtk.Attribute{default=rtk.CheckBox.UNCHECKED,calculate={[rtk.Attribute.NIL]=rtk.CheckBox.UNCHECKED,checked=rtk.CheckBox.static.CHECKED,unchecked=rtk.CheckBox.static.UNCHECKED,indeterminate=rtk.CheckBox.static.INDETERMINATE,}},icon=rtk.Attribute{default=function(self,attr)return self._value_map[rtk.CheckBox.UNCHECKED] +end,},surface=false,valign=rtk.Widget.TOP,wrap=true,tpadding=0,rpadding=0,lpadding=0,bpadding=0,}function rtk.CheckBox:initialize(attrs,...)if rtk.CheckBox.static._icon_unchecked==nil then +rtk.CheckBox._make_icons()end +self._value_map={[rtk.CheckBox.UNCHECKED]=rtk.CheckBox._icon_unchecked,[rtk.CheckBox.CHECKED]=rtk.CheckBox._icon_checked,[rtk.CheckBox.INDETERMINATE]=rtk.CheckBox._icon_intermediate +}rtk.Button.initialize(self,attrs,self.class.attributes.defaults,...)self:_handle_attr('value', self.calc.value)end +function rtk.CheckBox:_handle_click(event)local ret=rtk.Button._handle_click(self,event)if ret==false then +return ret +end +self:toggle()return ret +end +function rtk.CheckBox:_handle_attr(attr,value,oldval,trigger,reflow,sync)local ret=rtk.Button._handle_attr(self,attr,value,oldval,trigger,reflow,sync)if ret~=false then +if attr=='value' then +self.calc.icon=self._value_map[value] or self._value_map[rtk.CheckBox.UNCHECKED] +if trigger then +self:onchange()end +end +end +return ret +end +function rtk.CheckBox:_draw_icon(x,y,hovering,alpha)rtk.Button._draw_icon(self,x,y,hovering,alpha)if hovering then +rtk.CheckBox._icon_hover:draw(x,y,alpha,rtk.scale.value)end +end +function rtk.CheckBox:toggle()local value=self.calc.value +if self.calc.type==rtk.CheckBox.DUALSTATE then +if value==rtk.CheckBox.CHECKED then +value=rtk.CheckBox.UNCHECKED +else +value=rtk.CheckBox.CHECKED +end +else +if value==rtk.CheckBox.CHECKED then +value=rtk.CheckBox.INDETERMINATE +elseif value==rtk.CheckBox.INDETERMINATE then +value=rtk.CheckBox.UNCHECKED +else +value=rtk.CheckBox.CHECKED +end +end +self:sync('value', value)return self +end +function rtk.CheckBox:onchange()end +end)() + +__mod_rtk_application=(function() +local rtk=__mod_rtk_core +rtk.Application=rtk.class('rtk.Application', rtk.VBox)rtk.Application.register{status=rtk.Attribute{reflow=rtk.Widget.REFLOW_NONE +},statusbar=nil,toolbar=nil,screens=nil,}function rtk.Application:initialize(attrs,...)self.screens={stack={},}self.toolbar=rtk.HBox{bg=rtk.theme.bg,spacing=0,z=110,}self.toolbar:add(rtk.HBox.FLEXSPACE)self.statusbar=rtk.HBox{bg=rtk.theme.bg,lpadding=10,tpadding=5,bpadding=5,rpadding=10,z=110,}self.statusbar.text = self.statusbar:add(rtk.Text{color=rtk.theme.text_faded, text=""}, {expand=1})rtk.VBox.initialize(self,attrs,self.class.attributes.defaults,...)self:add(self.toolbar,{minw=150,bpadding=2})self:add(rtk.VBox.FLEXSPACE)self._content_position=#self.children +self:add(self.statusbar,{fillw=true})self:_handle_attr('status', self.calc.status)end +function rtk.Application:_handle_attr(attr,value,oldval,trigger,reflow,sync)local ok=rtk.VBox._handle_attr(self,attr,value,oldval,trigger,reflow,sync)if ok==false then +return ok +end +if attr=='status' then +self.statusbar.text:attr('text', value or ' ', nil, rtk.Widget.REFLOW_PARTIAL)end +return ok +end +function rtk.Application:add_screen(screen,name)assert(type(screen)=='table' and screen.init, 'screen must be a table containing an init() function')name=name or screen.name +assert(name, 'screen is missing name')assert(not self.screens[name], string.format('screen "%s" was already added', name))local widget=screen.init(self,screen)if widget then +assert(rtk.isa(widget, rtk.Widget), 'the return value from screen.init() must be type rtk.Widget (or nil)')screen.widget=widget +else +assert(rtk.isa(screen.widget, rtk.Widget), 'screen must contain a "widget" field of type rtk.Widget')end +screen.name=name +self.screens[name]=screen +if not screen.toolbar then +screen.toolbar=rtk.Spacer{h=0}end +self.toolbar:insert(1,screen.toolbar,{minw=50})screen.toolbar:hide()screen.widget:hide()if #self.screens.stack==0 then +self:replace_screen(screen)end +end +function rtk.Application:_show_screen(screen)screen=type(screen)=='table' and screen or self.screens[screen] +for _,s in ipairs(self.screens.stack)do +s.widget:hide()if s.toolbar then +s.toolbar:hide()end +end +assert(screen, 'screen not found, was add_screen() called?')if screen then +if screen.update then +screen.update(self,screen)end +if screen.widget.scrollto then +screen.widget:scrollto(0,0)end +screen.widget:show()self:replace(self._content_position,screen.widget,{expand=1,fillw=true,fillh=true,minw=screen.minw +})screen.toolbar:show()end +self:attr('status', nil)end +function rtk.Application:push_screen(screen)screen=type(screen)=='table' and screen or self.screens[screen] +assert(screen, 'screen not found, was add_screen() called?')if screen and #self.screens.stack>0 and self:current_screen()~=screen then +self:_show_screen(screen)self.screens.stack[#self.screens.stack+1]=screen +end +end +function rtk.Application:pop_screen()if #self.screens.stack>1 then +self:_show_screen(self.screens.stack[#self.screens.stack-1])table.remove(self.screens.stack)return true +else +return false +end +end +function rtk.Application:replace_screen(screen,idx)screen=type(screen)=='table' and screen or self.screens[screen] +assert(screen, 'screen not found, was add_screen() called?')local last=#self.screens.stack +idx=idx or last +if idx==0 then +idx=1 +end +if idx>=last then +self:_show_screen(screen)elseif screen.update then +screen.update(self,screen)end +self.screens.stack[idx]=screen +end +function rtk.Application:current_screen()local n=#self.screens.stack +if n>0 then +return self.screens.stack[#self.screens.stack] +end +end +end)() + +__mod_rtk_slider=(function() +local rtk=__mod_rtk_core +local log=__mod_rtk_log +rtk.Slider=rtk.class('rtk.Slider', rtk.Widget)rtk.Slider.static.TICKS_NEVER=0 +rtk.Slider.static.TICKS_ALWAYS=1 +rtk.Slider.static.TICKS_WHEN_ACTIVE=2 +rtk.Slider.register{[1]=rtk.Attribute{alias='value'},value=rtk.Attribute{default=0,priority=true,reflow=rtk.Widget.REFLOW_NONE,calculate=function(self,attr,value,target)return type(value)=='table' and value or {value}end,set=function(self,attr,value,calculated,target)self._use_scalar_value=type(value) ~='table'for i=1,#calculated do +calculated[i]=rtk.clamp(tonumber(calculated[i]),target.min,target.max)if not self._thumbs[i] then +self._thumbs[i]={idx=i,radius=0,radius_target=0}end +end +for i=#calculated+1,#self._thumbs do +self._thumbs[i]=nil +end +target.value=calculated +end +},color=rtk.Attribute{type='color',default=function(self,attr)return rtk.theme.slider +end,calculate=rtk.Reference('bg'),},trackcolor=rtk.Attribute{type='color',default=function(self,attr)return rtk.theme.slider_track +end,calculate=rtk.Reference('bg'),},thumbsize=rtk.Attribute{default=6,reflow=rtk.Widget.REFLOW_FULL,},thumbcolor=rtk.Attribute{type='color',},ticklabels=rtk.Attribute{reflow=rtk.Widget.REFLOW_FULL,},ticklabelcolor=rtk.Attribute {type='color',default=function(self,attr)return rtk.theme.slider_tick_label or rtk.theme.text +end,},spacing=rtk.Attribute{default=2,reflow=rtk.Widget.REFLOW_FULL,},ticks=rtk.Attribute{default=rtk.Slider.TICKS_NEVER,calculate={never=rtk.Slider.TICKS_NEVER,always=rtk.Slider.TICKS_ALWAYS,['when-active']=rtk.Slider.TICKS_WHEN_ACTIVE,['false']=rtk.Slider.TICKS_NEVER,[false]=rtk.Slider.TICKS_NEVER,['true']=rtk.Slider.TICKS_ALWAYS,[true]=rtk.Slider.TICKS_ALWAYS,},set=function(self,attr,value,calculated,target)self._tick_alpha=calculated==rtk.Slider.TICKS_ALWAYS and 1 or 0 +target.ticks=calculated +end,},ticksize=rtk.Attribute{default=4,reflow=rtk.Widget.REFLOW_FULL,},tracksize=rtk.Attribute{default=2,reflow=rtk.Widget.REFLOW_FULL,},min=0,max=100,step=rtk.Attribute{type='number',calculate=function(self,attr,value,target)return value and value>0 and value +end,},font=rtk.Attribute{default=function(self,attr)return self._theme_font[1] +end,reflow=rtk.Widget.REFLOW_FULL,},fontsize=rtk.Attribute{default=function(self,attr)return self._theme_font[2] +end,reflow=rtk.Widget.REFLOW_FULL,},fontscale=rtk.Attribute{default=1.0,reflow=rtk.Widget.REFLOW_FULL +},fontflags=rtk.Attribute{default=function(self,attr)return self._theme_font[3] +end +},focused_thumb_index=1,autofocus=true,scroll_on_drag=false,}function rtk.Slider:initialize(attrs,...)self._thumbs={}self._tick_alpha=0 +self._hovering_thumb=nil +self._font=rtk.Font()self._theme_font=rtk.theme.slider_font or rtk.theme.default_font +rtk.Widget.initialize(self,attrs,rtk.Slider.attributes.defaults,...)end +function rtk.Slider:_handle_attr(attr,value,oldval,trigger,reflow,sync)local ok=rtk.Widget._handle_attr(self,attr,value,oldval,trigger,reflow,sync)if ok==false then +return ok +end +if attr=='value' then +self:onchange()elseif self._label_segments and attr=='ticklabels' then +self._label_segments=nil +end +end +function rtk.Slider:_reflow(boxx,boxy,boxw,boxh,fillw,fillh,clampw,clamph,uiscale,viewport,window)local calc=self.calc +calc.x,calc.y=self:_get_box_pos(boxx,boxy)local w,h,tp,rp,bp,lp=self:_get_content_size(boxw,boxh,fillw,fillh,clampw,clamph)local hpadding=lp+rp +local vpadding=tp+bp +local lh=0 +local segments=self._label_segments +self._font:set(calc.font,calc.fontsize,calc.fontscale,calc.fontflags)if calc.step and calc.ticklabels and(not segments or not segments[1].isvalid())then +local lmaxw=(clampw or fillw)and(boxw-hpadding)or w or math.inf +local lmaxh=(clamph or fillh)and(boxh-vpadding)or h or math.inf +segments={}for n=1,#calc.ticklabels do +local label=calc.ticklabels[n] or ''local s,w,h=self._font:layout(label,lmaxw,lmaxh,false,rtk.Widget.CENTER,true,0,false +)s.w=w +s.h=h +segments[#segments+1]=s +lh=math.max(h,lh)end +lh=lh+calc.spacing +self._label_segments=segments +end +self.lh=lh +local minw=math.max(calc.minw or 0,#calc.value*calc.thumbsize*2)*rtk.scale.value +local minh=math.max(calc.minh or 0,calc.thumbsize*2,calc.tracksize)*rtk.scale.value +local size=math.max(calc.thumbsize*2,calc.ticksize,calc.tracksize)*rtk.scale.value +calc.w=w and(w+hpadding)or boxw +calc.h=h and(h+vpadding)or(size+self.lh+vpadding)calc.w=math.round(rtk.clamp(calc.w,minw,calc.maxw))calc.h=math.round(rtk.clamp(calc.h,minh,calc.maxh))return not w,false +end +function rtk.Slider:_realize_geometry()local calc=self.calc +local tp,rp,bp,lp=self:_get_padding_and_border()local scale=rtk.scale.value +local track={x=calc.x+lp+calc.thumbsize*scale,y=calc.y+tp+((calc.h-tp-bp-self.lh)-calc.tracksize*scale)/2,w=calc.w-lp-rp-calc.thumbsize*2*scale,h=calc.tracksize*scale,}local ticks +if calc.step then +ticks={distance=track.w/((calc.max-calc.min)/calc.step),size=calc.ticksize*scale,}ticks.offset=(ticks.size-track.h)/2 +for x=track.x,track.x+track.w+1,ticks.distance do +ticks[#ticks+1]={x-ticks.offset,track.y-ticks.offset}end +if calc.ticklabels then +local ly=track.y+calc.tracksize+(calc.spacing+calc.thumbsize)*scale +for n,segments in ipairs(self._label_segments)do +local tick=ticks[n] +if not tick then +break +end +segments.x=tick[1] +local offset=segments.w-ticks.size +if n==#ticks then +segments.x=segments.x-offset +elseif n>1 then +segments.x=segments.x-offset/2 +end +segments.y=ly +end +end +end +self._pre={tp=tp,rp=rp,bp=bp,lp=lp,track=track,ticks=ticks,}for idx=1,#self._thumbs do +self._thumbs[idx].value=nil +end +end +function rtk.Slider:_get_thumb(idx)assert(self._pre, '_get_thumb() called before reflow')local thumb=self._thumbs[idx] +local track=self._pre.track +local calc=self.calc +local value=calc.value[idx] +if thumb.value~=value then +thumb.pos=track.w*(value-calc.min)/(calc.max-calc.min)thumb.value=value +end +local c=self:calc('value')if c~=value then +thumb.pos_final=track.w*(c[idx]-calc.min)/(calc.max-calc.min)else +thumb.pos_final=thumb.pos +end +return thumb +end +function rtk.Slider:_get_nearest_thumb(clientx,clienty)local trackx=self.clientx+self._pre.lp +local tracky=self.clienty+self._pre.tp +local candidate=nil +local candidate_distance=nil +for i=1,#self._thumbs do +local thumb=self:_get_thumb(i)local delta=clientx-trackx-thumb.pos +local distance=math.abs(delta)if not candidate or(distance0)then +candidate=thumb +candidate_distance=distance +end +end +return candidate +end +function rtk.Slider:_clamp_value_to_step(v)local calc=self.calc +local step=calc.step +return rtk.clamp(step and(math.round(v/step)*step)or v,calc.min,calc.max)end +function rtk.Slider:_set_thumb_value(thumbidx,value,animate,fast)value=self:_clamp_value_to_step(value)local current=self:calc('value')if current[thumbidx]==value then +return false +end +local newval=self._use_scalar_value and value or table.shallow_copy(current,{[thumbidx]=value})if animate==false then +self:cancel_animation('value')self:sync('value', newval)else +self:sync('value', newval, current)local duration=fast and 0.25 or 0.4 +self:animate{'value', dst=newval, doneval=newval, duration=duration, easing='out-expo'}end +return true +end +function rtk.Slider:_set_thumb_value_with_crossover(idx,value,animate,event)local newidx +local calc=self.calc +if idx>1 and valuecalc.value[idx+1] then +newidx=idx+1 +end +if newidx then +self:_set_thumb_value(idx,calc.value[newidx],false)self.focused_thumb_index=newidx +self._hovering_thumb=newidx +self:_animate_thumb_overlays(event,nil,true)end +local changed=self:_set_thumb_value(self.focused_thumb_index,value,animate,event.type~=rtk.Event.KEY)return changed,self.focused_thumb_index +end +function rtk.Slider:_is_mouse_over(clparentx,clparenty,event)if not self.window or not self.window.in_window then +self._hovering_thumb=nil +return false +end +local calc=self.calc +local pre=self._pre +local y=calc.y+clparenty+pre.tp +local track=pre.track +local trackx=track.x+clparentx +local tracky=track.y+clparenty +local radius=20*rtk.scale.value +if not event:is_widget_pressed(self)then +self._hovering_thumb=nil +if rtk.point_in_box(event.x,event.y,trackx-radius,y-radius,calc.w+radius*2,calc.h+radius*2)then +for i=1,#self._thumbs do +local thumb=self:_get_thumb(i)if rtk.point_in_circle(event.x,event.y,trackx+thumb.pos,tracky,radius)then +self._hovering_thumb=i +break +end +end +else +return false +end +end +return self._hovering_thumb or +rtk.point_in_box(event.x,event.y,trackx,y-calc.thumbsize,calc.w,calc.h+calc.thumbsize*2)end +function rtk.Slider:_handle_mouseleave(event)local ok=rtk.Widget._handle_mouseleave(self,event)if ok==false then +return ok +end +self:_animate_thumb_overlays(event)return ok +end +function rtk.Slider:_handle_mousedown(event)local ok=rtk.Widget._handle_mousedown(self,event)if ok==false then +return ok +end +local thumb=self:_get_nearest_thumb(event.x,event.y)self.focused_thumb_index=thumb.idx +if not self._hovering_thumb then +local value=self:_get_value_from_offset(event.x-self.clientx-self.calc.thumbsize)self:_set_thumb_value(thumb.idx,value,true,true)else +self._hovering_thumb=thumb.idx +end +self:_animate_thumb_overlays(event)self:_animate_ticks(true)return true +end +function rtk.Slider:_handle_mouseup(event)local ok=rtk.Widget._handle_mouseup(self,event)self:_animate_thumb_overlays(event,nil,true)self:_animate_ticks(false)return ok +end +function rtk.Slider:_handle_dragstart(event,x,y,t)local draggable,droppable=self:ondragstart(self,event,x,y,t)if draggable~=nil then +return draggable,droppable +end +local thumb=self:_get_nearest_thumb(x,y)self.focused_thumb_index=thumb.idx +self:_animate_thumb_overlays(event,nil,true)return {startx=x,starty=y,thumbidx=thumb.idx},false +end +function rtk.Slider:_handle_dragmousemove(event,arg)local ok=rtk.Widget._handle_dragmousemove(self,event)if ok==false or event.simulated then +return ok +end +if not arg.startpos then +local thumb=self:_get_thumb(arg.thumbidx)arg.startpos=thumb.pos_final +end +local offx=(event.x-arg.startx)if arg.fine then +offx=math.ceil(offx*0.2)end +local v=self:_get_value_from_offset(offx+arg.startpos)local value_changed +value_changed,arg.thumbidx=self:_set_thumb_value_with_crossover(arg.thumbidx,v,self.calc.step~=nil,event)if(event.shift and value_changed)or(event.shift~=arg.fine)then +arg.startx=event.x +arg.starty=event.y +arg.startpos=nil +end +arg.fine=event.shift +event:set_handled(self)return true +end +function rtk.Slider:_handle_dragend(event,dragarg)self:_animate_ticks(false)end +function rtk.Slider:_handle_mousemove(event)self:_animate_thumb_overlays(event)end +function rtk.Slider:_handle_focus(event,context)self:_animate_thumb_overlays(event,true)return rtk.Widget._handle_focus(self,event,context)end +function rtk.Slider:_handle_blur(event,other)self._hovering_thumb=nil +self:_animate_thumb_overlays(event,false)return rtk.Widget._handle_blur(self,event,other)end +function rtk.Slider:_handle_keypress(event)local ok=rtk.Widget._handle_keypress(self,event)if ok==false or not self.focused_thumb_index then +return ok +end +local calc=self.calc +local value=calc.value[self.focused_thumb_index] +local step=calc.step or(calc.max-calc.min)/10 +if event.shift then +step=step*3 +elseif event.ctrl then +step=step*2 +end +local newvalue +if event.keycode==rtk.keycodes.LEFT or event.keycode==rtk.keycodes.DOWN then +newvalue=value-step +elseif event.keycode==rtk.keycodes.RIGHT or event.keycode==rtk.keycodes.UP then +newvalue=value+step +end +if newvalue then +self:_set_thumb_value_with_crossover(self.focused_thumb_index,newvalue,true,event)end +return ok +end +function rtk.Slider:_animate_thumb_overlays(event,focused,force)if rtk.dnd.dragging and not force then +return +end +if focused==nil then +focused=self.window.is_focused and self:focused()end +for i=1,#self._thumbs do +local dst=nil +local thumb=self:_get_thumb(i)if focused and thumb.idx==self.focused_thumb_index then +if event and event.buttons~=0 then +dst=32 +else +dst=20 +end +elseif thumb.idx==self._hovering_thumb then +dst=20 +elseif thumb.radius_target>0 then +dst=0 +end +if dst~=nil and dst~=thumb.radius_target then +thumb.radius_target=dst +rtk.queue_animation{key=string.format('%s.thumb.%d.hover', self.id, thumb.idx),src=thumb.radius,dst=dst,duration=0.2,easing='out-sine',update=function(val)thumb.radius=val +self:queue_draw()end,}end +end +end +function rtk.Slider:_animate_ticks(on)local calc=self.calc +if calc.step and calc.ticks==rtk.Slider.TICKS_WHEN_ACTIVE then +local dst=on and 1 or 0 +rtk.queue_animation{key=string.format('%s.ticks', self.id),src=self._tick_alpha,dst=dst,duration=0.2,easing='out-sine',update=function(val)self._tick_alpha=val +self:queue_draw()end,}else +self._ticks_alpha=(calc.ticks==rtk.Slider.TICKS_ALWAYS)and 1 or 0 +end +end +function rtk.Slider:_get_value_from_offset(offx)local calc=self.calc +local v=(offx*(calc.max-calc.min)/self._pre.track.w)+calc.min +return self:_clamp_value_to_step(v)end +function rtk.Slider:_draw(offx,offy,alpha,event,clipw,cliph,cltargetx,cltargety,parentx,parenty)rtk.Widget._draw(self,offx,offy,alpha,event,clipw,cliph,cltargetx,cltargety,parentx,parenty)local calc=self.calc +local y=calc.y+offy +if y+calc.h<0 or y>cliph or self.calc.ghost then +return false +end +local scale=rtk.scale.value +local pre=self._pre +local track=pre.track +local ticks=pre.ticks +local trackx=track.x+offx +local tracky=track.y+offy +local thumby=tracky+(track.h/2)local tickalpha=0.6*self._tick_alpha*alpha*calc.alpha +local drawticks=ticks and tickalpha>0 and not calc.disabled +self:_handle_drawpre(offx,offy,alpha,event)self:_draw_bg(offx,offy,alpha,event)self:setcolor(calc.trackcolor,alpha)gfx.rect(trackx,tracky,track.w,track.h,1)local first_thumb_x,last_thumb_x +if drawticks then +first_thumb_x=trackx+self:_get_thumb(1).pos +last_thumb_x=trackx+self:_get_thumb(#self._thumbs).pos +self:setcolor('black', tickalpha)for i=1,#ticks do +local x,y=table.unpack(ticks[i])if xlast_thumb_x then +gfx.rect(offx+x,offy+y,ticks.size,ticks.size,1)end +end +end +local thumbs={}local lastpos=0 +for i=1,#self._thumbs do +local thumb=self:_get_thumb(i)local thumbx=trackx+thumb.pos +if not calc.disabled then +if #self._thumbs==1 or i>1 then +local segmentw=thumb.pos-lastpos +self:setcolor(calc.color,alpha)gfx.rect(trackx+lastpos,tracky,segmentw,track.h,1)if drawticks then +self:setcolor('white', tickalpha)for j=math.floor(lastpos/ticks.distance)+(i>1 and 2 or 1),#ticks do +local x,y=table.unpack(ticks[j])if x>=track.x+thumb.pos then +break +end +gfx.rect(offx+x,offy+y,ticks.size,ticks.size,1)end +end +end +if thumb.radius>0 then +self:setcolor(calc.thumbcolor or calc.color,0.25*alpha)gfx.circle(thumbx,thumby,thumb.radius*scale,1,1)end +end +thumbs[#thumbs+1]={thumbx,thumby}lastpos=thumb.pos +end +if not calc.disabled then +self:setcolor(calc.thumbcolor or calc.color,alpha)end +for i=1,#thumbs do +local pos=thumbs[i] +gfx.circle(pos[1],pos[2],calc.thumbsize*scale,1,1)end +if self._label_segments then +if not calc.disabled then +self:setcolor(calc.ticklabelcolor,alpha)end +for n,segments in ipairs(self._label_segments)do +if not segments.x then +break +end +self._font:draw(segments,offx+segments.x,offy+segments.y)end +end +self:_draw_borders(offx,offy,alpha)self:_handle_draw(offx,offy,alpha,event)end +function rtk.Slider:onchange()end +end)() + +rtk.log=__mod_rtk_log +local function init()rtk.script_path=({reaper.get_action_context()})[2]:match('^.+[\\//]')rtk.reaper_hwnd=reaper.GetMainHwnd()local ver=reaper.GetAppVersion():lower()if ver:find('x64') or ver:find('arm64') or ver:find('_64') or ver:find('aarch64') then +rtk.os.bits=64 +end +local parts=ver:gsub('/.*', ''):split('.')rtk._reaper_version_major=tonumber(parts[1])local minor=parts[2] or ''local sepidx=minor:find('%D')if sepidx then +rtk._reaper_version_prerelease=minor:sub(sepidx):gsub('^%+', '')minor=minor:sub(1,sepidx-1)end +minor=tonumber(minor)or 0 +rtk._reaper_version_minor=minor<100 and minor or minor/10 +rtk.version.parse()rtk.scale._discover()if rtk.os.mac then +rtk.font.multiplier=0.75 +elseif rtk.os.linux then +rtk.font.multiplier=0.7 +end +rtk.set_theme_by_bgcolor(rtk.color.get_reaper_theme_bg() or '#262626')rtk.theme.default=true +end +init()return rtk +end)() +return rtk diff --git a/Envelopes/tilr_Snapshooter/tilr_Snapshooter apply snap 1.lua b/Envelopes/tilr_Snapshooter/tilr_Snapshooter apply snap 1.lua new file mode 100644 index 000000000..46027d9d5 --- /dev/null +++ b/Envelopes/tilr_Snapshooter/tilr_Snapshooter apply snap 1.lua @@ -0,0 +1,12 @@ +-- @noindex + +skip_init = true +local sep = package.config:sub(1, 1) +local script_folder = debug.getinfo(1).source:match("@?(.*[\\|/]).*[\\|/]") +local snapshooter = dofile(script_folder .. sep .. 'tilr_Snapshooter.lua') + +local snap = 1 +local hassnap = reaper.GetProjExtState(0, 'snapshooter', 'snap'..snap) ~= 0 +if hassnap then + snapshooter.applysnap(snap, false) +end diff --git a/Envelopes/tilr_Snapshooter/tilr_Snapshooter apply snap 2.lua b/Envelopes/tilr_Snapshooter/tilr_Snapshooter apply snap 2.lua new file mode 100644 index 000000000..0735bc329 --- /dev/null +++ b/Envelopes/tilr_Snapshooter/tilr_Snapshooter apply snap 2.lua @@ -0,0 +1,12 @@ +-- @noindex + +skip_init = true +local sep = package.config:sub(1, 1) +local script_folder = debug.getinfo(1).source:match("@?(.*[\\|/]).*[\\|/]") +local snapshooter = dofile(script_folder .. sep .. 'tilr_Snapshooter.lua') + +local snap = 2 +local hassnap = reaper.GetProjExtState(0, 'snapshooter', 'snap'..snap) ~= 0 +if hassnap then + snapshooter.applysnap(snap, false) +end diff --git a/Envelopes/tilr_Snapshooter/tilr_Snapshooter apply snap 3.lua b/Envelopes/tilr_Snapshooter/tilr_Snapshooter apply snap 3.lua new file mode 100644 index 000000000..9c452bea4 --- /dev/null +++ b/Envelopes/tilr_Snapshooter/tilr_Snapshooter apply snap 3.lua @@ -0,0 +1,12 @@ +-- @noindex + +skip_init = true +local sep = package.config:sub(1, 1) +local script_folder = debug.getinfo(1).source:match("@?(.*[\\|/]).*[\\|/]") +local snapshooter = dofile(script_folder .. sep .. 'tilr_Snapshooter.lua') + +local snap = 3 +local hassnap = reaper.GetProjExtState(0, 'snapshooter', 'snap'..snap) ~= 0 +if hassnap then + snapshooter.applysnap(snap, false) +end diff --git a/Envelopes/tilr_Snapshooter/tilr_Snapshooter apply snap 4.lua b/Envelopes/tilr_Snapshooter/tilr_Snapshooter apply snap 4.lua new file mode 100644 index 000000000..42a02a921 --- /dev/null +++ b/Envelopes/tilr_Snapshooter/tilr_Snapshooter apply snap 4.lua @@ -0,0 +1,12 @@ +-- @noindex + +skip_init = true +local sep = package.config:sub(1, 1) +local script_folder = debug.getinfo(1).source:match("@?(.*[\\|/]).*[\\|/]") +local snapshooter = dofile(script_folder .. sep .. 'tilr_Snapshooter.lua') + +local snap = 4 +local hassnap = reaper.GetProjExtState(0, 'snapshooter', 'snap'..snap) ~= 0 +if hassnap then + snapshooter.applysnap(snap, false) +end diff --git a/Envelopes/tilr_Snapshooter/tilr_Snapshooter save snap 1.lua b/Envelopes/tilr_Snapshooter/tilr_Snapshooter save snap 1.lua new file mode 100644 index 000000000..9916e9c29 --- /dev/null +++ b/Envelopes/tilr_Snapshooter/tilr_Snapshooter save snap 1.lua @@ -0,0 +1,8 @@ +-- @noindex + +skip_init = true +local sep = package.config:sub(1, 1) +local script_folder = debug.getinfo(1).source:match("@?(.*[\\|/]).*[\\|/]") +local snapshooter = dofile(script_folder .. sep .. 'tilr_Snapshooter.lua') + +snapshooter.savesnap(1) diff --git a/Envelopes/tilr_Snapshooter/tilr_Snapshooter save snap 2.lua b/Envelopes/tilr_Snapshooter/tilr_Snapshooter save snap 2.lua new file mode 100644 index 000000000..848d5781c --- /dev/null +++ b/Envelopes/tilr_Snapshooter/tilr_Snapshooter save snap 2.lua @@ -0,0 +1,8 @@ +-- @noindex + +skip_init = true +local sep = package.config:sub(1, 1) +local script_folder = debug.getinfo(1).source:match("@?(.*[\\|/]).*[\\|/]") +local snapshooter = dofile(script_folder .. sep .. 'tilr_Snapshooter.lua') + +snapshooter.savesnap(2) diff --git a/Envelopes/tilr_Snapshooter/tilr_Snapshooter save snap 3.lua b/Envelopes/tilr_Snapshooter/tilr_Snapshooter save snap 3.lua new file mode 100644 index 000000000..885d3399e --- /dev/null +++ b/Envelopes/tilr_Snapshooter/tilr_Snapshooter save snap 3.lua @@ -0,0 +1,8 @@ +-- @noindex + +skip_init = true +local sep = package.config:sub(1, 1) +local script_folder = debug.getinfo(1).source:match("@?(.*[\\|/]).*[\\|/]") +local snapshooter = dofile(script_folder .. sep .. 'tilr_Snapshooter.lua') + +snapshooter.savesnap(3) diff --git a/Envelopes/tilr_Snapshooter/tilr_Snapshooter save snap 4.lua b/Envelopes/tilr_Snapshooter/tilr_Snapshooter save snap 4.lua new file mode 100644 index 000000000..41020bfc7 --- /dev/null +++ b/Envelopes/tilr_Snapshooter/tilr_Snapshooter save snap 4.lua @@ -0,0 +1,8 @@ +-- @noindex + +skip_init = true +local sep = package.config:sub(1, 1) +local script_folder = debug.getinfo(1).source:match("@?(.*[\\|/]).*[\\|/]") +local snapshooter = dofile(script_folder .. sep .. 'tilr_Snapshooter.lua') + +snapshooter.savesnap(4) diff --git a/Envelopes/tilr_Snapshooter/tilr_Snapshooter write snap 1.lua b/Envelopes/tilr_Snapshooter/tilr_Snapshooter write snap 1.lua new file mode 100644 index 000000000..1e6e74cb8 --- /dev/null +++ b/Envelopes/tilr_Snapshooter/tilr_Snapshooter write snap 1.lua @@ -0,0 +1,12 @@ +-- @noindex + +skip_init = true +local sep = package.config:sub(1, 1) +local script_folder = debug.getinfo(1).source:match("@?(.*[\\|/]).*[\\|/]") +local snapshooter = dofile(script_folder .. sep .. 'tilr_Snapshooter.lua') + +local snap = 1 +local hassnap = reaper.GetProjExtState(0, 'snapshooter', 'snap'..snap) ~= 0 +if hassnap then + snapshooter.applysnap(snap, true) +end diff --git a/Envelopes/tilr_Snapshooter/tilr_Snapshooter write snap 2.lua b/Envelopes/tilr_Snapshooter/tilr_Snapshooter write snap 2.lua new file mode 100644 index 000000000..f902fd38c --- /dev/null +++ b/Envelopes/tilr_Snapshooter/tilr_Snapshooter write snap 2.lua @@ -0,0 +1,12 @@ +-- @noindex + +skip_init = true +local sep = package.config:sub(1, 1) +local script_folder = debug.getinfo(1).source:match("@?(.*[\\|/]).*[\\|/]") +local snapshooter = dofile(script_folder .. sep .. 'tilr_Snapshooter.lua') + +local snap = 2 +local hassnap = reaper.GetProjExtState(0, 'snapshooter', 'snap'..snap) ~= 0 +if hassnap then + snapshooter.applysnap(snap, true) +end diff --git a/Envelopes/tilr_Snapshooter/tilr_Snapshooter write snap 3.lua b/Envelopes/tilr_Snapshooter/tilr_Snapshooter write snap 3.lua new file mode 100644 index 000000000..657b365d1 --- /dev/null +++ b/Envelopes/tilr_Snapshooter/tilr_Snapshooter write snap 3.lua @@ -0,0 +1,12 @@ +-- @noindex + +skip_init = true +local sep = package.config:sub(1, 1) +local script_folder = debug.getinfo(1).source:match("@?(.*[\\|/]).*[\\|/]") +local snapshooter = dofile(script_folder .. sep .. 'tilr_Snapshooter.lua') + +local snap = 3 +local hassnap = reaper.GetProjExtState(0, 'snapshooter', 'snap'..snap) ~= 0 +if hassnap then + snapshooter.applysnap(snap, true) +end diff --git a/Envelopes/tilr_Snapshooter/tilr_Snapshooter write snap 4.lua b/Envelopes/tilr_Snapshooter/tilr_Snapshooter write snap 4.lua new file mode 100644 index 000000000..a93abe3a2 --- /dev/null +++ b/Envelopes/tilr_Snapshooter/tilr_Snapshooter write snap 4.lua @@ -0,0 +1,12 @@ +-- @noindex + +skip_init = true +local sep = package.config:sub(1, 1) +local script_folder = debug.getinfo(1).source:match("@?(.*[\\|/]).*[\\|/]") +local snapshooter = dofile(script_folder .. sep .. 'tilr_Snapshooter.lua') + +local snap = 4 +local hassnap = reaper.GetProjExtState(0, 'snapshooter', 'snap'..snap) ~= 0 +if hassnap then + snapshooter.applysnap(snap, true) +end