diff --git a/Tracks/tilr_Keyboard splitter.lua b/Tracks/tilr_Keyboard splitter.lua new file mode 100644 index 000000000..bb51c6f50 --- /dev/null +++ b/Tracks/tilr_Keyboard splitter.lua @@ -0,0 +1,608 @@ +-- @description Keyboard splitter +-- @author tilr +-- @version 1.0 +-- @provides tilr_Keyboard splitter/rtk.lua +-- @screenshot https://raw.githubusercontent.com/tiagolr/keyboard-splitter/master/doc/keyboard-splitter.gif +-- @about +-- # Keyboard splitter +-- +-- Manage tracks keyboard splitting using a visual keymap. +-- Requires JSFX MIDI Keyvel Filter installed. + +function log(t) + reaper.ShowConsoleMsg(t .. '\n') +end +function logtable(table) + log(tostring(table)) + for index, value in pairs(table) do -- print table + log(' ' .. tostring(index) .. ' : ' .. tostring(value)) + end +end +function clone_table(table) + local copy = {} + for key, val in pairs(table) do + copy[key] = val + end + return copy +end + +local sep = package.config:sub(1, 1) +local script_folder = debug.getinfo(1).source:match("@?(.*[\\|/])") +rtk = dofile(script_folder .. 'tilr_Keyboard splitter' .. sep .. 'rtk.lua') + +globals = { + win_x = nil, + win_y = nil, + win_w = 768, + win_h = 385, + key_h = 30, + key_w = 6, + region_h = 254, + vel_h = 2, + drag_margin = 10, +} +g = globals + +-- init globals from project config +local exists, win_x = reaper.GetProjExtState(0, 'keyboard_splitter', 'win_x') +if exists ~= 0 then globals.win_x = tonumber(win_x) end +local exists, win_y = reaper.GetProjExtState(0, 'keyboard_splitter', 'win_y') +if exists ~= 0 then globals.win_y = tonumber(win_y) end + +_notes = {'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'} +notes = {} +for i = 0, 127 do notes[i+1] = _notes[i % 12 + 1] .. (math.floor(i/12) - 1) end +colors = {'cyan', 'coral', 'dodgerblue', 'chartreuse', 'deeppink', 'floralwhite', 'yellow', 'floralwhite', 'lightcyan', 'lightgreen', 'violet', 'teal', 'salmon', 'paleturquoise', 'lawngreen', 'mintcream'} +sel_tracks = {} +keyvel_count = 0 +widget_drag = { + active = false, + wideg = nil, + start_y = 0, + start_val = 0 +} + +sel_key = nil +regions = {} +mouse = { + down = false, + toggled = false, + double_click_timer = 0, + drag = { + active = false, + start_x = 0, + start_y = 0, + region = nil, + margin = nil, + } +} + +function make_region(keymin, keymax, velmin, velmax) + return { + id = rtk.uuid4(), + keymin = keymin, + keymax = keymax, + velmin = velmin, + velmax = velmax, + x = keymin * g.key_w, + y = g.win_h - (velmax * g.vel_h + g.key_h), + w = (keymax - keymin) * g.key_w + g.key_w, + h = (velmax - velmin) * g.vel_h + g.vel_h, + transpose = 0, + hover = false, + selected = false, + updated = false, + track = 0, + fxid = '', + file = '', + } +end + +function draw_keyboard() + function draw_key (x, y, w, h, black_key) + if black_key then + gfx.set(0, 0, 0) + gfx.rect(x, y, w, h, 1) + else + gfx.set(1, 1, 1) + gfx.rect(x, y, w, h, 1) + end + end + for i=0, 127 do + local pitch = i % 12 + local is_black_key = pitch == 1 or pitch == 3 or pitch == 6 or pitch == 8 or pitch == 10 + draw_key(i * globals.key_w, globals.win_h - globals.key_h, globals.key_w, globals.key_h, is_black_key) + end +end + +function draw_pitch_key() + for _, reg in ipairs(regions) do + gfx.set(1, .5, 0, 1) + gfx.rect(reg.keymin * g.key_w, g.win_h - g.key_h, g.key_w, g.key_h) + end +end + +function draw_guides() + for i=0, 127, 12 do + gfx.set(1, 1, 1, .25) + gfx.line(i * g.key_w, g.win_h - g.region_h - g.key_h, i*g.key_w, g.win_h - g.key_h) + gfx.x = i * g.key_w + 5 + gfx.y = g.win_h - g.key_h - g.region_h + gfx.drawstr('C'..(math.floor(i/12) - 1)) + end +end + +function draw_regions() + local helper_w = 6 + for _, reg in ipairs(regions) do + local r, g, b, _ = rtk.color.rgba(colors[(reg.track % #colors) + 1]) + gfx.set(r, g, b, reg.selected and 0.5 or 0.25) + gfx.rect(reg.x, reg.y, reg.w, reg.h, 1) + gfx.set(r, g, b, (reg.hover or reg.selected) and 0.75 or 0.5) + gfx.rect(reg.x, reg.y, reg.w, reg.h, 0) + if reg.hover then -- draw drag helpers + gfx.rect(reg.x, reg.y + reg.h / 2 - helper_w / 2, helper_w, helper_w, 1) -- left + gfx.rect(reg.x + reg.w - helper_w, reg.y + reg.h / 2 - helper_w / 2, helper_w, helper_w, 1) -- right + gfx.rect(reg.x + reg.w / 2 - helper_w / 2, reg.y, helper_w, helper_w, 1) -- top + gfx.rect(reg.x + reg.w / 2 - helper_w / 2, reg.y + reg.h - helper_w, helper_w, helper_w, 1) -- bottom + end + end +end + +-- recalc regions after window resize +function recalc_regions () + for _, reg in ipairs(regions) do + rr = make_region(reg.keymin, reg.keymax, reg.velmin, reg.velmax) + reg.x = rr.x + reg.y = rr.y + reg.w = rr.w + reg.h = rr.h + end +end + +function select_region(reg) + local index = -1 + for i,r in ipairs(regions) do + r.selected = r == reg + if r.selected then index = i end + end + if index > -1 then -- move region to top of the list + table.remove(regions, index) + table.insert(regions, reg) + end +end + +function is_keyvel(tr, nfx) + local ret, pname = reaper.TrackFX_GetParamName(tr, nfx, 0) + local ret2, pname2 = reaper.TrackFX_GetParamName(tr, nfx, 4) + return ret and ret2 and pname == 'Note min' and pname2 == 'Transpose' +end + +function has_keyvel (tr) + for i = 1, reaper.TrackFX_GetCount(tr) do + if is_keyvel(tr, i - 1) then + return true + end + end + return false +end + +-- create regions/keyvel for selected tracks without keyvel control +function create_regions () + reaper.Undo_BeginBlock() + for i = 1, reaper.GetNumTracks() do + local track = reaper.GetTrack(0, i - 1) + if reaper.IsTrackSelected(track) and not has_keyvel(track) then + local fxi = reaper.TrackFX_AddByName(track, 'MIDI Keyvel Filter', false, -1000) + if fxi == -1 then + reaper.MB('MIDI Keyvel Filter JSFX is missing. Make sure you install it via ReaPack.', '', 0) + reaper.Undo_EndBlock('keysplitter - create_regions', 0) + return + end + reaper.TrackFX_Show(track, fxi, 0) + reaper.TrackFX_Show(track, fxi, 2) + end + end + reaper.Undo_EndBlock('keysplitter - create_regions', 0) +end + +function update_region_from_fx(reg, track, nfx, ntrack) + local _, keymin = reaper.TrackFX_GetFormattedParamValue(track, nfx, 0) + local _, keymax = reaper.TrackFX_GetFormattedParamValue(track, nfx, 1) + local _, velmin = reaper.TrackFX_GetFormattedParamValue(track, nfx, 2) + local _, velmax = reaper.TrackFX_GetFormattedParamValue(track, nfx, 3) + local _, transpose = reaper.TrackFX_GetFormattedParamValue(track, nfx, 4) + local rr = make_region(tonumber(keymin), tonumber(keymax), tonumber(velmin), tonumber(velmax)) + reg.keymin = rr.keymin + reg.keymax = rr.keymax + reg.velmin = rr.velmin + reg.velmax = rr.velmax + reg.x = rr.x + reg.y = rr.y + reg.w = rr.w + reg.h = rr.h + reg.transpose = transpose + reg.track = ntrack + reg.updated = true +end + +function create_region_from_fx(track, nfx, ntrack) + local _, keymin = reaper.TrackFX_GetFormattedParamValue(track, nfx, 0) + local _, keymax = reaper.TrackFX_GetFormattedParamValue(track, nfx, 1) + local _, velmin = reaper.TrackFX_GetFormattedParamValue(track, nfx, 2) + local _, velmax = reaper.TrackFX_GetFormattedParamValue(track, nfx, 3) + local _, transpose = reaper.TrackFX_GetFormattedParamValue(track, nfx, 4) + local fxid = reaper.TrackFX_GetFXGUID(track, nfx) + local reg = make_region(tonumber(keymin), tonumber(keymax), tonumber(velmin), tonumber(velmax)) + reg.track = ntrack + reg.fxid = fxid + reg.transpose = transpose + local _, trackname = reaper.GetTrackName(track) + reg.trackname = trackname + return reg +end + +function fetch_regions() + sel_tracks = {} + keyvel_count = 0 + local regions_map = {} + local new_regions = {} + for _, reg in ipairs(regions) do + reg.updated = false + regions_map[reg.fxid] = reg + end + for i = 1, reaper.GetNumTracks() do + local track = reaper.GetTrack(0, i - 1) + if reaper.IsTrackSelected(track) then + table.insert(sel_tracks, track) + for j = 1, reaper.TrackFX_GetCount(track) do + if is_keyvel(track, j - 1) then + keyvel_count = keyvel_count + 1 + local fxid = reaper.TrackFX_GetFXGUID(track, j - 1) + local reg = regions_map[fxid] + if reg then + update_region_from_fx(regions_map[fxid], track, j - 1, i - 1) + else + reg = create_region_from_fx(track, j - 1, i - 1) + table.insert(new_regions, reg) + end + goto continue + end + end + :: continue :: + end + end + for _, reg in ipairs(regions) do + if reg.updated then + table.insert(new_regions, reg) + end + end + regions = new_regions +end + +function update_keyvel_from_reg(reg) + for i = 1, reaper.GetNumTracks() do + local track = reaper.GetTrack(0, i - 1) + for j = 1, reaper.TrackFX_GetCount(track) do + local fxid = reaper.TrackFX_GetFXGUID(track, j - 1) + if fxid == reg.fxid then + reaper.TrackFX_SetParam(track, j - 1, 0, reg.keymin) + reaper.TrackFX_SetParam(track, j - 1, 1, reg.keymax) + reaper.TrackFX_SetParam(track, j - 1, 2, reg.velmin) + reaper.TrackFX_SetParam(track, j - 1, 3, reg.velmax) + reaper.TrackFX_SetParam(track, j - 1, 4, reg.transpose) + return + end + end + end +end + +function start_drag(region, margin) + mouse.drag.active = true + mouse.drag.region = clone_table(region) -- region copy + mouse.drag.start_x = rtk.mouse.x + mouse.drag.start_y = rtk.mouse.y + mouse.drag.margin = margin +end + +function stop_drag() + mouse.drag.active = false + mouse.drag.region = nil + mouse.drag.margin = nil +end + +function update_drag() + if not mouse.drag.active then return end + local reg = mouse.drag.region + local delta_x = rtk.mouse.x - mouse.drag.start_x + local delta_y = rtk.mouse.y - mouse.drag.start_y + local keymin = mouse.drag.region.keymin + local keymax = mouse.drag.region.keymax + local velmin = mouse.drag.region.velmin + local velmax = mouse.drag.region.velmax + if mouse.drag.margin == 'left' then + keymin = mouse.drag.region.keymin + math.floor(delta_x / g.key_w) + elseif mouse.drag.margin == 'right' then + keymax = mouse.drag.region.keymax + math.floor(delta_x / g.key_w) + elseif mouse.drag.margin == 'top' then + velmax = velmax - math.floor(delta_y / g.vel_h) + elseif mouse.drag.margin == 'bottom' then + velmin = velmin - math.floor(delta_y / g.vel_h) + else + keymin = keymin + math.floor(delta_x / g.key_w) + keymax = keymax + math.floor(delta_x / g.key_w) + velmin = velmin - math.floor(delta_y / g.vel_h) + velmax = velmax - math.floor(delta_y / g.vel_h) + end + if keymin > keymax then + local tmp = keymin + keymin = keymax + keymax = tmp + end + if velmin > velmax then + local tmp = velmin + velmin = velmax + velmax = tmp + end + if keymin < 0 then -- fix out of bounds drag + if not mouse.drag.margin then keymax = keymax - keymin end + keymin = 0 + end + if keymax > 127 then -- fix out of bounds drag + if not mouse.drag.margin then keymin = keymin + 127 - keymax end + keymax = 127 + end + if velmin < 0 then -- + if not mouse.drag.margin then velmax = velmax - velmin end + velmin = 0 + end + if velmax > 127 then -- + if not mouse.drag.margin then velmin = velmin + 127 - velmax end + velmax = 127 + end + local newreg = make_region(keymin, keymax, velmin, velmax) + for _, rr in ipairs(regions) do + if rr.id == reg.id then + rr.keymin = newreg.keymin + rr.keymax = newreg.keymax + rr.x = newreg.x + rr.w = newreg.w + rr.velmin = newreg.velmin + rr.velmax = newreg.velmax + rr.y = newreg.y + rr.h = newreg.h + update_keyvel_from_reg(rr) + end + end +end + +function update_mouse() + if rtk.mouse.down == 1 then + if not mouse.down then + mouse.toggled = true + end + mouse.down = true + else + mouse.down = false + end + local hover_margin = nil + local hover = false + local selected = false + if mouse.drag.active then + goto continue + end + for i = #regions, 1, -1 do + local reg = regions[i] + reg.hover = false + if not hover and rtk.point_in_box(rtk.mouse.x, rtk.mouse.y, reg.x, reg.y, reg.w, reg.h) then -- mouse in region + reg.hover = true + hover = true + if mouse.toggled then + selected = reg + end + if rtk.point_in_box(rtk.mouse.x, rtk.mouse.y, reg.x, reg.y + reg.h / 2 - g.drag_margin / 2, g.drag_margin, g.drag_margin) then -- mouse in left drag + hover_margin = 'left' + elseif rtk.point_in_box(rtk.mouse.x, rtk.mouse.y, reg.x + reg.w - g.drag_margin, reg.y + reg.h / 2 - g.drag_margin / 2, g.drag_margin, g.drag_margin) then -- mouse in right drag + hover_margin = 'right' + elseif rtk.point_in_box(rtk.mouse.x, rtk.mouse.y, reg.x + reg.w / 2 - g.drag_margin / 2, reg.y, g.drag_margin, g.drag_margin) then -- mouse in top drag + hover_margin = 'top' + elseif rtk.point_in_box(rtk.mouse.x, rtk.mouse.y, reg.x + reg.w / 2 - g.drag_margin / 2, reg.y + reg.h - g.drag_margin, g.drag_margin, g.drag_margin) then -- mouse in bottom drag + hover_margin = 'bottom' + end + end + end + ::continue:: + if selected then + select_region(selected) + start_drag(selected, hover_margin) + end + if not hover and not mouse.drag.margin then + window:request_mouse_cursor(rtk.mouse.cursors.POINTER) + end + if mouse.drag.margin or hover_margin then -- if its dragging or hovering margins draw cursor + if mouse.drag.margin == 'left' or hover_margin == 'left' or mouse.drag.margin == 'right' or hover_margin == 'right' then + window:request_mouse_cursor(rtk.mouse.cursors.SIZE_EW) + elseif mouse.drag.margin == 'top' or hover_margin == 'top' or mouse.drag.margin == 'bottom' or hover_margin == 'bottom' then + window:request_mouse_cursor(rtk.mouse.cursors.SIZE_NS) + end + end + if mouse.drag.active and not mouse.down then + stop_drag() + end + if not selected and mouse.toggled and rtk.point_in_box(rtk.mouse.x, rtk.mouse.y, 0, g.win_h - g.region_h - g.key_h - 50, g.win_w, g.win_h) then -- mouse in regions area + select_region(nil) + end +end + +function popup_fx(fxid, show_first_instrument) + for i = 1, reaper.GetNumTracks() do + local track = reaper.GetTrack(0, i - 1) + for j = 1, reaper.TrackFX_GetCount(track) do + local _fxid = reaper.TrackFX_GetFXGUID(track, j - 1) + if _fxid == fxid then + if show_first_instrument then + local inst = reaper.TrackFX_GetInstrument(track) + reaper.TrackFX_Show(track, inst > -1 and inst or j - 1, 1) + else + reaper.TrackFX_Show(track, j - 1, 1) + end + goto continue + end + end + end + :: continue :: +end + +function on_double_click() + for i = #regions, 1, -1 do + local reg = regions[i] + if rtk.point_in_box(rtk.mouse.x, rtk.mouse.y, reg.x, reg.y, reg.w, reg.h) then + popup_fx(reg.fxid, true) + goto continue + end + end + :: continue :: +end + +function start_widget_drag(widget) + local reg + for _, r in ipairs(regions) do + if (r.selected) then reg = r end + end + if not reg then return end + widget_drag.active = true + widget_drag.widget = widget + widget_drag.start_y = rtk.mouse.y + widget_drag.start_val = reg[widget] +end + +function update_widget_drag() + if not widget_drag.active then return end + if not mouse.down then + widget_drag.active = false + return + end + local offset_y = math.floor((widget_drag.start_y - rtk.mouse.y) / 2) + local val = widget_drag.start_val + offset_y + if widget_drag.widget == 'transpose' then + if val < -60 then val = -60 end + if val > 60 then val = 60 end + else + if val < 0 then val = 0 end + if val > 127 then val = 127 end + end + local reg + for _, r in ipairs(regions) do + if (r.selected) then reg = r end + end + if not reg then return end + if widget_drag.widget == 'velmin' and val > reg.velmax then val = reg.velmax end + if widget_drag.widget == 'velmax' and val < reg.velmin then val = reg.velmin end + if widget_drag.widget == 'keymin' and val > reg.keymax then val = reg.keymax end + if widget_drag.widget == 'keymax' and val < reg.keymin then val = reg.keymin end + reg[widget_drag.widget] = val + update_keyvel_from_reg(reg) +end + +function draw() + fetch_regions() + update_mouse() + update_drag() + draw_keyboard() + draw_pitch_key() + draw_guides() + draw_regions() + draw_ui() + update_widget_drag() + if mouse.toggled then + local time = reaper.time_precise() + if time - mouse.double_click_timer < 0.25 then + on_double_click() + end + mouse.double_click_timer = reaper.time_precise() + end + mouse.toggled = false +end + +function draw_ui() + local sel_region + for _, reg in ipairs(regions) do + if reg.selected then sel_region = reg end + end + if not sel_region then + ui_controls:attr('visible', false) + ui_helpbox:attr('visible', true) + local help_text = '' + if #sel_tracks == 0 then + help_text = 'No tracks selected' + elseif keyvel_count == 0 then + help_text = 'No regions found' + end + ui_helpbox:attr('text', help_text) + else + ui_controls:attr('visible', true) + ui_helpbox:attr('visible', false) + ui_note_start:attr('text', math.floor(sel_region.keymin) .. ' ' .. notes[sel_region.keymin + 1]) + ui_note_end:attr('text', math.floor(sel_region.keymax) .. ' ' .. notes[sel_region.keymax + 1]) + ui_vel_min:attr('text', math.floor(sel_region.velmin)) + ui_vel_max:attr('text', math.floor(sel_region.velmax)) + ui_transpose:attr('text', math.floor(sel_region.transpose)) + ui_track_name:attr('text', sel_region.trackname) + end + ui_btn_create_regions:attr('disabled', #sel_tracks <= keyvel_count) +end + +function init() + window = rtk.Window{ w=globals.win_w, h=globals.win_h, title='Keyboard splitter'} + window.onmove = function (self) + reaper.SetProjExtState(0, 'keyboard_splitter', 'win_x', self.x) + reaper.SetProjExtState(0, 'keyboard_splitter', 'win_y', self.y) + end + window.onupdate = function () + window:queue_draw() + end + window.ondraw = draw + + window.onresize = function () + globals.win_w = window.w + globals.win_h = window.h + globals.key_w = window.w / 128 + recalc_regions() + end + + ui_controls = window:add(rtk.VBox{ padding=10, spacing=10 }) + ui_hbox = ui_controls:add(rtk.HBox{ spacing=10 }) + ui_hbox:add(rtk.Text{'Vel min'}) + ui_vel_min = ui_hbox:add(rtk.Text{'', w=40, cursor=rtk.mouse.cursors.SIZE_NS }) + ui_vel_min.onmousedown = function () start_widget_drag('velmin') end + ui_hbox:add(rtk.Text{'Vel max'}) + ui_vel_max = ui_hbox:add(rtk.Text{'', w=40, cursor=rtk.mouse.cursors.SIZE_NS }) + ui_vel_max.onmousedown = function () start_widget_drag('velmax') end + ui_hbox:add(rtk.Text{'Note start'}) + ui_note_start = ui_hbox:add(rtk.Text{'', w=60, cursor=rtk.mouse.cursors.SIZE_NS }) + ui_note_start.onmousedown = function () start_widget_drag('keymin') end + ui_hbox:add(rtk.Text{'Note end'}) + ui_note_end = ui_hbox:add(rtk.Text{'', w=60, cursor=rtk.mouse.cursors.SIZE_NS }) + ui_note_end.onmousedown = function () start_widget_drag('keymax') end + ui_track_name = ui_controls:add(rtk.Text{''}) + ui_hbox:add(rtk.Text{'Transpose'}) + ui_transpose = ui_hbox:add(rtk.Text{'', w=60, cursor=rtk.mouse.cursors.SIZE_NS }) + ui_transpose.onmousedown = function () start_widget_drag('transpose') end + + + ui_helpbox = window:add(rtk.Text{'No region selected', padding=10}) + + ui_right_side = window:add(rtk.HBox{w=g.win_w, padding=10}) + ui_right_side:add(rtk.Box.FLEXSPACE) + ui_btn_create_regions = ui_right_side:add(rtk.Button{'Create regions'}) + ui_btn_create_regions.onclick = create_regions + + window:open{align='center'} + if globals.win_x and globals.win_y then + window:attr('x', globals.win_x) + window:attr('y', globals.win_y) + end + +end + +init() diff --git a/Tracks/tilr_Keyboard splitter/rtk.lua b/Tracks/tilr_Keyboard splitter/rtk.lua new file mode 100644 index 000000000..aaefe773b --- /dev/null +++ b/Tracks/tilr_Keyboard splitter/rtk.lua @@ -0,0 +1,6089 @@ +-- @noindex + +-- This is generated code. See https://reapertoolkit.dev/ for more info. +-- version: 1.3.0 +-- build: Sun Nov 20 21:44:10 UTC 2022 +__RTK_VERSION='1.3.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={},wall_time_start=os.time(),reaper_time_start=reaper.time_precise(),}log.wall_time_start=log.wall_time_start+(log.reaper_time_start-math.floor(log.reaper_time_start))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,window=nil,has_js_reascript_api=(reaper.JS_Window_GetFocus~=nil),has_sws_extension=(reaper.BR_Win32_GetMonitorRectFromRect~=nil),script_path=nil,reaper_hwnd=nil,tick=0,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=math.ceil(tonumber(dpi)/rtk.scale.reaper)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={},latest=nil},last={},}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},accent='#47abff',accent_subtle='#306088',tooltip_bg='#ffffff',tooltip_text='#000000',tooltip_font={'Segoe UI (TrueType)', 16},text='#ffffff',text_faded='#bbbbbb',text_font=nil,button='#555555',heading=nil,heading_font={'Calibri', 26},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,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_overlay='#00000040',popup_bg_brightness=1.3,popup_shadow='#11111166',popup_border='#385074',slider='#2196f3',slider_track='#5a5a5a',slider_font=nil,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 +rtk.theme[k]={rtk.color.rgba(v)}end +end +end +function rtk.add_image_search_path(path,iconstyle)path=path:gsub('[/\\]$', '') .. '/'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')else +iconstyle='nostyle'end +local paths=rtk._image_paths[iconstyle] +if not paths then +paths={}rtk._image_paths[iconstyle]=paths +end +paths[#paths+1]=path +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 state=rtk.mouse.state[rtk.mouse.state.latest] +if state then +state.modaltick=rtk.tick +end +local widgets={...}for _,widget in ipairs(widgets)do +rtk._modal[widget.id]={widget,rtk.tick}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.isrel(value)return value and value>0 and value<=1.0 +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 +function string.count(s,sub)local c=-1 +local idx=0 +while idx do +_,idx=s:find(sub,idx+1)c=c+1 +end +return c +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,exterior +if anim.stepfunc then +newval,exterior=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=newval +if exterior==nil and anim.calculate then +value=anim.calculate(widget,attr,newval,widget.calc)exterior=value +end +widget.calc[attr]=value +if anim.sync_exterior_value then +widget[attr]=exterior or value +end +else +widget:attr(attr,anim.doneval or exterior)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 +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 or 1 +end +local r,g,b,a2=rtk.color.hex2rgba(resolved)return r,g,b,a or a2 +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)if not color then +return under and rtk.color.luma(under)or 0 +end +local r,g,b,a=rtk.color.rgba(color)local luma=(0.2126*r+0.7152*g+0.0722*b)if a<1.0 then +luma=math.abs((luma*a)+(under and(rtk.color.luma(under)*(1-a))or 0))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={transparent="#ffffff00",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=math.ceil(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.WINDOWCLOSE=7 +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.WINDOWCLOSE]='windowclose',}rtk.Event.register{type=nil,handled=nil,button=0,buttons=0,wheel=0,hwheel=0,char=nil,keycode=nil,keynorm=nil,ctrl=false,shift=false,alt=false,meta=false,modifiers=nil,files=nil,x=nil,y=nil,time=0,tick=nil,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 +self.tick=rtk.tick +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.calc.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 +local keynorm_map={[33]=49,[64]=50,[35]=51,[36]=52,[37]=53,[94]=54,[38]=55,[42]=56,[40]=57,[41]=48,[126]=96,[95]=45,[43]=61,[123]=91,[125]=93,[58]=59,[34]=39,[60]=44,[62]=46,[63]=47,}function rtk.Event:set_keycode(keycode)self.keycode=math.ceil(keycode)self.keynorm=keycode +if keycode<=26 and self.ctrl then +self.keynorm=keycode+96 +self.char=string.char(self.keynorm)elseif keycode>=65 and keycode<=90 then +self.keynorm=keycode+32 +self.char=string.char(keycode)elseif keycode>=32 and keycode~=127 then +if keycode<=255 then +self.keynorm=keynorm_map[keycode] or self.keycode +self.char=string.char(self.keycode)elseif keycode<=282 then +self.keynorm=keycode-160 +self.char=string.char(self.keynorm)elseif keycode<=346 then +self.keynorm=keycode-224 +self.char=string.char(self.keynorm)end +end +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 +event.tick=rtk.tick +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(id,fname,paths)if not paths or #paths==0 then +return +end +local path=paths[1]..fname +local r=gfx.loadimg(id,path)if r~=-1 then +return path +end +if #paths>1 then +for i=2,#paths do +path=paths[i]..fname +r=gfx.loadimg(id,path)if r~=-1 then +return path +end +end +end +end +function rtk.Image.static._search_image_paths_nostyle(id,fname)local path=_search_image_paths_list(id,fname,rtk._image_paths.nostyle)return path or _search_image_paths_list(id,fname,rtk._image_paths.fallback)end +function rtk.Image.static._search_image_paths_style(id,fname,style)local path=_search_image_paths_list(id,fname,rtk._image_paths[style])if path then +return path,style +end +end +function rtk.Image.static._search_image_paths(id,fname,style)local path,gotstyle +if not style then +path,gotstyle=rtk.Image._search_image_paths_nostyle(id,fname)if not path then +style=rtk.theme.iconstyle +path,gotstyle=rtk.Image._search_image_paths_style(id,fname,style)end +else +path,gotstyle=rtk.Image._search_image_paths_style(id,fname,style)if not path then +path,gotstyle=rtk.Image._search_image_paths_nostyle(id,fname)end +end +if not path then +local other=(style=='light') and 'dark' or 'light'path,gotstyle=rtk.Image._search_image_paths_style(id,fname,other)end +return path,gotstyle +end +function rtk.Image.static.icon(name,style)style=style or rtk.theme.iconstyle +local pack=rtk.Image._icons[name] +if pack then +local img=pack:get(name,style)if img then +return img +end +end +if not name:find('%.[%w_]+$') then +name=name .. '.png'end +local img,gotstyle=rtk.Image():_load(name,style)if img then +if gotstyle and gotstyle~=style 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 could not be loaded from any image 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 ok,gotstyle=self:_load(path,nil,density)if ok then +return self +else +log.warning('rtk: rtk.Image:load("%s"): no such file found in any search paths', path)end +end +function rtk.Image:_load(fname,style,density)local id=self.id +if not id or self._ref then +id=rtk.Image.static.ids:next()end +local path,gotstyle=rtk.Image._search_image_paths(id,fname,style)if path then +self.id=id +self.path=path +self.w,self.h=gfx.getimgdim(id)self.density=density or 1.0 +return self,gotstyle +else +rtk.Image.static.ids:release(id)self.w,self.h=nil,nil +self.id=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)local rotation=attrs.rotation and math.rad(attrs.rotation)or self._rotation_rads +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 rotation==0 or not rotation 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,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=degrees +local rads=math.rad(degrees)self._rotation_rads=rads +local x1,y1=0,0 +local xt1,yt1=_xlate(x1,y1,rads)local x2,y2=0+self.w,0 +local xt2,yt2=_xlate(x2,y2,rads)local x3,y3=0,self.h +local xt3,yt3=_xlate(x3,y3,rads)local x4,y4=0+self.w,self.h +local xt4,yt4=_xlate(x4,y4,rads)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=false +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,reflow_uses_exterior_value=true,},y=rtk.Attribute{default=0,reflow=rtk.Widget.REFLOW_FULL,reflow_uses_exterior_value=true,},w=rtk.Attribute{type='number',reflow=rtk.Widget.REFLOW_FULL,reflow_uses_exterior_value=true,animate=function(self,anim,scale)local calculated=anim.resolve(anim.easingfunc(anim.pct))local exterior +if anim.doneval and anim.doneval~=rtk.Attribute.NIL and anim.doneval~=rtk.Attribute.DEFAULT then +exterior=(anim.pct<1 and calculated or anim.doneval)/(scale or rtk.scale.value)end +if anim.dst==0 or anim.dst>1 then +exterior = (type(exterior) == 'number' and exterior > 0 and exterior <= 1.0) and 1.01 or exterior +end +return calculated,exterior +end,},h=rtk.Attribute{type='number',reflow=rtk.Widget.REFLOW_FULL,reflow_uses_exterior_value=true,animate=rtk.Reference('w'),},z=rtk.Attribute{default=0,reflow=rtk.Widget.REFLOW_FULL},minw = rtk.Attribute{type='number', reflow=rtk.Widget.REFLOW_FULL, reflow_uses_exterior_value=true},minh = rtk.Attribute{type='number', reflow=rtk.Widget.REFLOW_FULL, reflow_uses_exterior_value=true},maxw = rtk.Attribute{type='number', reflow=rtk.Widget.REFLOW_FULL, reflow_uses_exterior_value=true},maxh = rtk.Attribute{type='number', reflow=rtk.Widget.REFLOW_FULL, reflow_uses_exterior_value=true},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=rtk.Attribute{default=false,reflow=rtk.Widget.REFLOW_NONE,},tooltip=nil,cursor=nil,alpha=rtk.Attribute{default=1.0,reflow=rtk.Widget.REFLOW_NONE,},autofocus=nil,bg=rtk.Attribute{reflow=rtk.Widget.REFLOW_NONE,calculate=function(self,attr,value,target,animation)if not value and animation then +local parent=self.parent +value=parent and parent.calc.bg or rtk.theme.bg +end +return value and {rtk.color.rgba(value)}end,},hotzone=rtk.Attribute{reflow=rtk.Widget.REFLOW_NONE,replaces={'thotzone', 'rhotzone', 'bhotzone', 'lhotzone'},get=function(self,attr,target)return {target.thotzone,target.rhotzone,target.bhotzone,target.lhotzone}end,calculate=function(self,attr,value,target)local t,r,b,l=rtk.Widget.static._calc_padding_or_margin(value)target.thotzone,target.rhotzone,target.bhotzone,target.lhotzone=t,r,b,l +target._hotzone_set=true +return {t,r,b,l}end +},thotzone=rtk.Attribute{priority=true,reflow=rtk.Widget.REFLOW_NONE,calculate=function(self,attr,value,target)target._hotzone_set=true +return value +end,},rhotzone=rtk.Reference('thotzone'),bhotzone=rtk.Reference('thotzone'),lhotzone=rtk.Reference('thotzone'),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,}local _refs_metatable={__mode='v',__index=function(table,key)return table.__self:_ref(table,key)end,__newindex=function(table,key,value)rawset(table,key,value)table.__empty=false +end +}local _calc_metatable={__call=function(table,_,attr,instant)return table.__self:_calc(attr,instant)end +}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=setmetatable({__empty=true,__self=self},_refs_metatable)self.calc=setmetatable({__self=self,border_uniform=true},_calc_metatable)local clsattrs=self.class.attributes +local tables={clsattrs.defaults,...}local merged={}for n=1,#tables do +for k,v in pairs(tables[n])do +merged[k]=v +end +end +if attrs then +for k,v in pairs(attrs)do +local meta=clsattrs[k] or rtk.Attribute.NIL +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 +if not tonumber(k)then +merged[k]=v +end +end +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 clsattrs=self.class.attributes +local priority={}local calc=self.calc +for k,v in pairs(attrs)do +local meta=clsattrs[k] +if meta and not meta.priority then +if v==rtk.Attribute.FUNCTION then +v=clsattrs[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 +if #priority==0 then +return +end +for _,k in ipairs(priority)do +local v=self[k] +if v==rtk.Attribute.FUNCTION then +v=clsattrs[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.calc.w then +x=self.window.calc.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(event)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 +if self.parent then +self.parent:_set_focused_child(self)end +self:queue_draw()return true +end +return false +end +function rtk.Widget:blur(event,other)if not self:focused(event)then +return true +end +if self:_handle_blur(event,other)~=false then +rtk.focused=nil +if self.parent then +self.parent:_set_focused_child(nil)end +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 and meta.animate~=rtk.Attribute.NIL)and meta.animate +kwargs.calculate=meta.calculate +kwargs.sync_exterior_value=meta.reflow_uses_exterior_value +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 calcsrc,calcdst +local doneval=kwargs.dst or rtk.Attribute.DEFAULT +if attr == 'w' or attr == 'h' then +if(not kwargs.src or kwargs.src==rtk.Attribute.NIL)or(kwargs.src<=1.0 and kwargs.src>=0)then +if kwargs.src==rtk.Attribute.NIL then +kwargs.src=nil +end +kwargs.src=(calc[attr] or 0)*(kwargs.src or 1)calcsrc=true +end +if(not kwargs.dst or kwargs.dst==rtk.Attribute.NIL)or(kwargs.dst<=1.0 and kwargs.dst>0)then +if kwargs.dst==rtk.Attribute.NIL then +kwargs.dst=nil +end +local current=self[attr] +local current_calc=calc[attr] +self[attr]=kwargs.dst +calc[attr]=meta.calculate and meta.calculate(self,attr,kwargs.dst,{},true)or 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 +calcdst=true +self[attr]=current +calc[attr]=current_calc +window:reflow(rtk.Widget.REFLOW_FULL)end +end +if not calcdst and meta.calculate then +kwargs.dst=meta.calculate(self,attr,kwargs.dst,{},true)doneval=kwargs.dst or rtk.Attribute.DEFAULT +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)calc[attr]=kwargs.src +calcsrc=kwargs.src~=nil +end +if not calcsrc and meta.calculate then +kwargs.src=meta.calculate(self,attr,kwargs.src,{},true)calc[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,greedyw,greedyh)local calc=self.calc +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,greedyw,greedyh}expw,exph=self:_reflow(boxx,boxy,boxw,boxh,fillw,fillh,clampw,clamph,uiscale,viewport,window,greedyw,greedyh)end +self.realized=true +self:onreflow()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:_adjscale(val,scale,box)if not val then +return +elseif val>0 and val<=1.0 and box then +return val*box +elseif(self.calc.scalability&rtk.Widget.FULL~=rtk.Widget.FULL)then +return val +else +return val*(scale or rtk.scale.value)end +end +function rtk.Widget:_get_box_pos(boxx,boxy)local x=self.x or 0 +local y=self.y or 0 +if self.calc.scalability&rtk.Widget.FULL==rtk.Widget.FULL then +local scale=rtk.scale.value +return scale*x+boxx,scale*y+boxy +else +return x+boxx,y+boxy +end +end +local function _get_content_dimension(size,box,padding,fill,clamp,greedy,scale)if size then +if box and size<-1 then +return box+(size*scale)-padding +elseif box and size<=1.0 then +return greedy and math.abs(box*size)-padding +else +return(size*scale)-padding +end +end +if fill and box and greedy then +return box-padding +end +end +function rtk.Widget:_get_content_size(boxw,boxh,fillw,fillh,clampw,clamph,scale,greedyw,greedyh)scale=self:_adjscale(scale or 1)local tp,rp,bp,lp=self:_get_padding_and_border()local w=_get_content_dimension(self.w,boxw,lp+rp,fillw,clampw,greedyw,scale)local h=_get_content_dimension(self.h,boxh,tp+bp,fillh,clamph,greedyh,scale)local minw,maxw,minh,maxh=self:_get_min_max_sizes(boxw,boxh,greedyw,greedyh,scale)maxw=maxw and clampw and math.min(maxw,boxw)or maxw +maxh=maxh and clamph and math.min(maxh,boxh)or maxh +minw=minw and minw-lp-rp +maxw=maxw and maxw-lp-rp +minh=minh and minh-tp-bp +maxh=maxh and maxh-tp-bp +return w,h,tp,rp,bp,lp,minw,maxw,minh,maxh +end +function rtk.Widget:_get_min_max_sizes(boxw,boxh,greedyw,greedyh,scale)local minw,maxw,minh,maxh=self.minw,self.maxw,self.minh,self.maxh +return minw and((minw>1 or minw<=0)and(minw*scale)or(greedyw and minw*boxw)),maxw and((maxw>1 or maxw<=0)and(maxw*scale)or(greedyw and maxw*boxw)),minh and((minh>1 or minh<=0)and(minh*scale)or(greedyh and minh*boxh)),maxh and((maxh>1 or maxh<=0)and(maxh*scale)or(greedyh and maxh*boxh))end +function rtk.Widget:_reflow(boxx,boxy,boxw,boxh,fillw,fillh,clampw,clamph,uiscale,viewport,window,greedyw,greedyh)local calc=self.calc +calc.x,calc.y=self:_get_box_pos(boxx,boxy)local w,h,tp,rp,bp,lp,minw,maxw,minh,maxh=self:_get_content_size(boxw,boxh,fillw,fillh,clampw,clamph,nil,greedyw,greedyh +)calc.w=rtk.clamp(w or(fillw and greedyw and(boxw-lp-rp)or 0),minw,maxw)+lp+rp +calc.h=rtk.clamp(h or(fillh and greedyh and(boxh-tp-bp)or 0),minh,maxh)+tp+bp +return fillw and greedyw,fillh and greedyh +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 +local w,h=calc.w,calc.h +if calc._hotzone_set then +local scale=rtk.scale.value +local l=(calc.lhotzone or 0)*scale +local t=(calc.thotzone or 0)*scale +x=x-l +y=y-t +w=w+l+(calc.rhotzone or 0)*scale +h=h+t+(calc.bhotzone or 0)*scale +end +return self.window and self.window.in_window and +rtk.point_in_box(event.x,event.y,x,y,w,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,tooltip)tooltip=tooltip or self.calc.tooltip +local font=rtk.Font(table.unpack(rtk.theme.tooltip_font))local segments,w,h=font:layout(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 +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 +elseif thickness==0 then +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(event)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(event)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 event.handled and self.mouseover then +self.mouseover=false +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 +else +if event.handled then +self:_handle_mouseleave(event)self.hovering=false +self.mouseover=false +self:queue_draw()else +self.mouseover=true +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)~=false 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(event)then +event:set_handled(self)end +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&2~=0 then +if state&16==0 and not dnd.dragging then +if self:_handle_click(event)then +event:set_handled(self)self:queue_draw()end +local last=rtk.mouse.last[event.button] +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 +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 +else +self.mouseover=false +end +if rtk.touchscroll and event.type==rtk.Event.MOUSEUP and self:focused(event)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 then +if self:focused(event)and self:_handle_keypress(event)then +event:set_handled(self)self:queue_draw()end +end +if event.type==rtk.Event.WINDOWCLOSE then +self:_handle_windowclose(event)end +if(self.mouseover or dnd.dragging==self)and calc.cursor then +self.window:request_mouse_cursor(calc.cursor)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)|2|4)self._last_mousedown_time=0 +else +event:set_button_state(self,(state or 0)|2)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 +local autofocus=self.calc.autofocus +if autofocus or +(autofocus==nil and self.onclick~=rtk.Widget.onclick)then +self:focus(event)return ok or self:focused(event)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 +function rtk.Widget:_handle_windowclose(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_AUTO=2 +rtk.Viewport.static.SCROLLBAR_ALWAYS=3 +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,auto=rtk.Viewport.SCROLLBAR_AUTO,},},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,}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')if rtk.focused==oldval then +self:_set_focused_child(nil)end +end +if value then +value.viewport=self +value.parent=self +value.window=self.window +self:_sync_child_refs(value, 'add')if rtk.focused==value then +self:_set_focused_child(value)end +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.16*(1+offset^0.2)self._scrollbar_alpha_hover=0.40*(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:_set_focused_child(child)return rtk.Container._set_focused_child(self,child)end +function rtk.Viewport:focused(event)return rtk.Container.focused(self,event)end +function rtk.Viewport:remove()self:attr('child', nil)end +function rtk.Viewport:_reflow(boxx,boxy,boxw,boxh,fillw,fillh,clampw,clamph,uiscale,viewport,window,greedyw,greedyh)local calc=self.calc +calc.x,calc.y=self:_get_box_pos(boxx,boxy)local w,h,tp,rp,bp,lp,minw,maxw,minh,maxh=self:_get_content_size(boxw,boxh,fillw,fillh,clampw,clamph,nil,greedyw,greedyh +)local hpadding=lp+rp +local vpadding=tp+bp +local inner_maxw=rtk.clamp(w or(boxw-hpadding),minw,maxw)local inner_maxh=rtk.clamp(h or(boxh-vpadding),minh,maxh)local scrollw,scrollh=0,0 +if calc.vscrollbar==rtk.Viewport.SCROLLBAR_ALWAYS or +(calc.vscrollbar==rtk.Viewport.SCROLLBAR_AUTO and self._vscrollh>0)then +scrollw=calc.scrollbar_size*rtk.scale.value +inner_maxw=inner_maxw-scrollw +end +if calc.hscrollbar==rtk.Viewport.SCROLLBAR_ALWAYS or +(calc.hscrollbar==rtk.Viewport.SCROLLBAR_AUTO and self._hscrollh>0)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=self:_reflow_child(inner_maxw,inner_maxh,uiscale,window,greedyw,greedyh)local pass2=false +if calc.vscrollbar==rtk.Viewport.SCROLLBAR_AUTO then +if scrollw==0 and wh>inner_maxh then +scrollw=calc.scrollbar_size*rtk.scale.value +inner_maxw=inner_maxw-scrollw +pass2=ww>inner_maxw +elseif scrollw>0 and wh<=inner_maxh then +pass2=ww>=inner_maxw +inner_maxw=inner_maxw+scrollw +scrollw=0 +end +end +if pass2 then +wx,wy,ww,wh=self:_reflow_child(inner_maxw,inner_maxh,uiscale,window,greedyw,greedyh)end +if greedyw then +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 +end +if greedyh then +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 +end +ccalc.x=wx +ccalc.y=wy +child:_realize_geometry()innerw=math.ceil(rtk.clamp(ww+wx,fillw and greedyw and inner_maxw,inner_maxw))innerh=math.ceil(rtk.clamp(wh+wy,fillh and greedyh and inner_maxh,inner_maxh))else +innerw,innerh=inner_maxw,inner_maxh +hmargin,vmargin=0,0 +end +calc.w=rtk.clamp((w or(innerw+scrollw+hmargin))+hpadding,minw,maxw)calc.h=rtk.clamp((h or(innerh+scrollh+vmargin))+vpadding,minh,maxh)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:_reflow_child(maxw,maxh,uiscale,window,greedyw,greedyh)local calc=self.calc +return calc.child:reflow(0,0,maxw,maxh,false,false,not calc.flexw,not calc.flexh,uiscale,self,window,greedyw,greedyh +)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 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,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)if self._vscrolla.current==0 or self._vscrollh==0 then +return +end +local calc=self.calc +local scrx=offx+self._vscrollx +local scry=offy+calc.y+calc.h*calc.scroll_top/self.child.calc.h +self:setcolor(self._scrollbar_color,self._vscrolla.current*alpha)gfx.rect(scrx,scry,calc.scrollbar_size*rtk.scale.value,self._vscrollh+1,1)end +function rtk.Viewport:_calc_scrollbar_alpha(clparentx,clparenty,event,dragchild)local calc=self.calc +if calc.vscrollbar==rtk.Viewport.SCROLLBAR_NEVER then +return +end +local dragself=(rtk.dnd.dragging==self)local alpha=0 +local duration=0.2 +if self._vscrollh>0 then +if not rtk._modal or rtk.is_modal(self)then +local overthumb=event:get_button_state(self.id)if self.mouseover then +if overthumb==nil and self._vscroll_in_gutter then +overthumb=rtk.point_in_box(event.x,event.y,clparentx+self._vscrollx,clparenty+calc.y+calc.h*calc.scroll_top/self.child.calc.h,calc.scrollbar_size*rtk.scale.value,self._vscrollh +)end +if event.type==rtk.Event.MOUSEDOWN then +event:set_button_state(self.id,overthumb)end +end +if self._vscroll_in_gutter or dragself then +if overthumb then +alpha=self._scrollbar_alpha_hover +duration=0.1 +else +alpha=self._scrollbar_alpha_proximity +end +elseif self.mouseover or calc.vscrollbar==rtk.Viewport.SCROLLBAR_ALWAYS then +alpha=self._scrollbar_alpha_proximity +elseif dragchild and dragchild.show_scrollbar_on_drag then +alpha=self._scrollbar_alpha_proximity +duration=0.15 +end +elseif calc.vscrollbar==rtk.Viewport.SCROLLBAR_ALWAYS then +alpha=self._scrollbar_alpha_proximity +end +end +if alpha~=self._vscrolla.target then +if alpha==0 then +duration=0.3 +end +rtk.queue_animation{key=string.format('%s.vscrollbar', self.id),src=self._vscrolla.current,dst=alpha,duration=duration,update=function(value)self._vscrolla.current=value +self:queue_draw()end,}self._vscrolla.target=alpha +self:queue_draw()end +end +function rtk.Viewport:_handle_event(clparentx,clparenty,event,clipped,listen)local calc=self.calc +local pre=self._pre +listen=self:_should_handle_event(listen)local x=calc.x+clparentx +local y=calc.y+clparenty +local hovering=rtk.point_in_box(event.x,event.y,x,y,calc.w,calc.h)and self.window.in_window +local dragging=rtk.dnd.dragging +local is_child_dragging=dragging and dragging.viewport==self +local child=self.child +if event.type==rtk.Event.MOUSEMOVE then +self._vscroll_in_gutter=false +if listen and is_child_dragging and dragging.scroll_on_drag then +if event.y-20y+calc.h then +self:scrollby(0,math.max(5,math.abs(y+calc.h-event.y)),false)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 +self._vscroll_in_gutter=true +if event.x>=self._vscrollx+clparentx then +event:set_handled(self)end +end +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 +child and child.visible and child.realized then +self:_clamp()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 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 +listen=rtk.Widget._handle_event(self,clparentx,clparenty,event,clipped,listen)self.mouseover=self.mouseover or(child and child.mouseover)self:_calc_scrollbar_alpha(clparentx,clparenty,event,is_child_dragging and dragging)return 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 target +if natural then +target=calc.scroll_top+(voffset-event.y)else +local pct=rtk.clamp(event.y-self.clienty-voffset,0,calc.h)/calc.h +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.AUTOCLOSE_DISABLED=0 +rtk.Popup.AUTOCLOSE_LOCAL=1 +rtk.Popup.AUTOCLOSE_GLOBAL=2 +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.Attribute{default=function(self,attr)return rtk.theme.popup_overlay +end,calculate=rtk.Reference('bg'),},autoclose=rtk.Attribute{default=rtk.Popup.AUTOCLOSE_LOCAL,calculate={['disabled']=rtk.Popup.AUTOCLOSE_DISABLED,['local']=rtk.Popup.AUTOCLOSE_LOCAL,['global']=rtk.Popup.AUTOCLOSE_GLOBAL,[true]=rtk.Popup.AUTOCLOSE_LOCAL,[false]=rtk.Popup.AUTOCLOSE_DISABLED,}},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,...)self._popup_visible=false +end +function rtk.Popup:_handle_event(clparentx,clparenty,event,clipped,listen)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 +return listen +end +function rtk.Popup:_reflow(boxx,boxy,boxw,boxh,fillw,fillh,clampw,clamph,uiscale,viewport,window,greedyw,greedyh)local calc=self.calc +local anchor=calc.anchor +if anchor then +local y=anchor.clienty +local wh=self.window.calc.h +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 +for k,v in pairs(child.refs)do +if k ~= '__self' and k ~= '__empty' then +w.refs[k]=v +end +end +w=w.parent +end +else +for k in pairs(child.refs)do +self.refs[k]=nil +end +end +end +end +function rtk.Container:_set_focused_child(child)local w=self +while w do +w._focused_child=child +w=w.parent +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')if rtk.focused==child then +self:_set_focused_child(child)end +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')if rtk.focused==child then +self:_set_focused_child(nil)end +return child +end +end +function rtk.Container:focused(event)return rtk.focused==self or(event and event.type==rtk.Event.KEY and rtk.focused and rtk.focused==self._focused_child +)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], calculated, nil, 'cell', widget)end +return calculated +end +function rtk.Container:_reorder(srcidx,targetidx)if srcidx~=nil and srcidx~=targetidx then +local widgetattrs=table.remove(self.children,srcidx)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(widget,targetidx)local srcidx=self:get_child_index(widget)return self:_reorder(srcidx,targetidx)end +function rtk.Container:reorder_before(widget,target)local srcidx=self:get_child_index(widget)local targetidx=self:get_child_index(target)if not srcidx or not targetidx then +return false +end +return self:_reorder(srcidx,targetidx>srcidx and targetidx-1 or targetidx)end +function rtk.Container:reorder_after(widget,target)local srcidx=self:get_child_index(widget)local targetidx=self:get_child_index(target)if not srcidx or not targetidx then +return false +end +return self:_reorder(srcidx,srcidx>targetidx and targetidx+1 or targetidx)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 chmouseover +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 +local wx,wy +if widget.calc.position&rtk.Widget.POSITION_FIXED~=0 and self.viewport then +local vcalc=self.viewport.calc +wx,wy=x+vcalc.scroll_left,y+vcalc.scroll_top +else +wx,wy=x,y +end +self:_handle_event_child(clparentx,clparenty,event,clipped,listen,wx,wy,widget,attrs)chmouseover=chmouseover or widget.mouseover +end +end +end +listen=rtk.Widget._handle_event(self,clparentx,clparenty,event,clipped,listen)self.mouseover=self.mouseover or chmouseover +return listen +end +function rtk.Container:_handle_event_child(clparentx,clparenty,event,clipped,listen,wx,wy,child,attrs)return child:_handle_event(wx,wy,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,greedyw,greedyh)local calc=self.calc +local x,y=self:_get_box_pos(boxx,boxy)local w,h,tp,rp,bp,lp,minw,maxw,minh,maxh=self:_get_content_size(boxw,boxh,fillw and greedyw,fillh and greedyh,clampw,clamph,nil,greedyw,greedyh +)local inner_maxw=rtk.clamp(w or(boxw-lp-rp),minw,maxw)local inner_maxh=rtk.clamp(h or(boxh-tp-bp),minh,maxh)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)attrs._minw=self:_adjscale(attrs.minw,uiscale,greedyw and inner_maxw)attrs._maxw=self:_adjscale(attrs.maxw,uiscale,greedyh and inner_maxw)attrs._minh=self:_adjscale(attrs.minh,uiscale,greedyh and inner_maxh)attrs._maxh=self:_adjscale(attrs.maxh,uiscale,greedyh and inner_maxh)local wx,wy,ww,wh=widget:reflow(0,0,rtk.clamp(inner_maxw-widget.x-clp-crp,attrs._minw,attrs._maxw),rtk.clamp(inner_maxh-widget.y-ctp-cbp,attrs._minh,attrs._maxh),attrs.fillw,attrs.fillh,clampw or attrs.maxw~=nil,clamph or attrs.maxh~=nil,uiscale,viewport,window,greedyw,greedyh +)ww=math.max(ww,attrs._minw or 0)wh=math.max(wh,attrs._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 or not greedyw 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 or not greedyh 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=math.ceil(rtk.clamp((w or innerw)+lp+rp,minw,maxw))calc.h=math.ceil(rtk.clamp((h or innerh)+tp+bp,minh,maxh))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',default=rtk.Attribute.NIL,reflow=rtk.Widget.REFLOW_NONE,redraw=false,window_sync=true,},y=rtk.Attribute{type='number',default=rtk.Attribute.NIL,reflow=rtk.Widget.REFLOW_NONE,redraw=false,window_sync=true,},w=rtk.Attribute{priority=true,type='number',window_sync=true,reflow_uses_exterior_value=true,animate=function(self,anim)return rtk.Widget.attributes.w.animate(self,anim,rtk.scale.framebuffer)end,calculate=function(self,attr,value,target)return value and value*rtk.scale.framebuffer +end,},h=rtk.Attribute{priority=true,type='number',window_sync=true,reflow_uses_exterior_value=true,animate=rtk.Reference('w'),calculate=rtk.Reference('w'),},minw=rtk.Attribute{default=100,window_sync=true,reflow_uses_exterior_value=true,},minh=rtk.Attribute{default=30,window_sync=true,reflow_uses_exterior_value=true,},maxw=rtk.Attribute{window_sync=true,reflow_uses_exterior_value=true,},maxh=rtk.Attribute{window_sync=true,reflow_uses_exterior_value=true,},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')},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=true +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 +self._run_queued=self.running +end +function rtk.Window:_get_display_resolution(working,frame)local x=math.floor(self.x or 0)local y=math.floor(self.y or 0)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)local sw=r-l +local sh=math.abs(b-t)if frame then +local borderless=self.calc.borderness +sw=sw-(borderless and 0 or self._os_window_frame_width)sh=sh-(borderless and 0 or self._os_window_frame_height)end +return l,t,sw,sh +end +function rtk.Window:_get_relative_size_from_display(w,h)local sz=w or h +if sz>0 and sz<=1.0 then +local _,_,sw,sh=self:_get_display_resolution(true,not self.calc.borderless)return w and sw*w or sh*h +else +return sz +end +end +function rtk.Window:_get_geometry_from_attrs(overrides)overrides=overrides or {}local scale=rtk.scale.framebuffer or 1 +local minw,maxw,minh,maxh,sx,sy,sw,sh=self:_get_min_max_sizes()if not sh then +sx,sy,sw,sh=self:_get_display_resolution(true,not self.calc.borderless)end +local calc=self.calc +local x=self.x +local y=self.y +if not x then +x=0 +overrides.halign=rtk.Widget.CENTER +end +if not y then +y=0 +overrides.valign=rtk.Widget.CENTER +end +local w=rtk.isrel(self.w)and(self.w*sw)or(calc.w/scale)local h=rtk.isrel(self.h)and(self.h*sh)or(calc.h/scale)w=rtk.clamp(w,minw and minw/scale,maxw and maxw/scale)h=rtk.clamp(h,minh and minh/scale,maxh and maxh/scale)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 +return math.round(x),math.round(y),math.round(w),math.round(h)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:reflow(rtk.Widget.REFLOW_FULL)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_GetRect(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 +sh=h+self._os_window_frame_height +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 +local calc=self.calc +rtk.window=self +if options then +options.halign=options.halign or options.align +options.valign=options.valign or options.align +end +if not calc.borderless and self._os_window_frame_width==0 then +self:_discover_os_window_frame_size(rtk.reaper_hwnd)end +if not self.w or not self.h then +self:reflow(rtk.Widget.REFLOW_FULL)end +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 +if not self._run_queued then +self:_run()end +end +function rtk.Window:_close()self.running=false +gfx.quit()end +function rtk.Window:close()local event=rtk.Event{type=rtk.Event.WINDOWCLOSE}self:_handle_window_event(event,reaper.time_precise())self.hwnd=nil +self:_close()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:queue_mouse_refresh()self._mouse_refresh_queued=true +end +function rtk.Window:_get_content_size(boxw,boxh,fillw,fillh,clampw,clamph,scale,greedyw,greedyh)local calc=self.calc +local tp,rp,bp,lp=self:_get_padding_and_border()local w=rtk.isrel(self.w)and(self.w*boxw)or(self.w and(calc.w-lp-rp))or nil +local h=rtk.isrel(self.h)and(self.h*boxh)or(self.h and(calc.h-tp-bp))or nil +local minw,maxw,minh,maxh=self:_get_min_max_sizes(boxw,boxh,greedyw,greedyh,scale)return w,h,tp,rp,bp,lp,minw,maxw,minh,maxh +end +function rtk.Window:_get_min_max_sizes(boxw,boxh,greedyw,greedyh,scale)if not self._sync_window_attrs_on_update then +return +end +local calc=self.calc +local sx,sy,sw,sh=self:_get_display_resolution(true,not calc.borderless)scale=rtk.scale.framebuffer +local minw,maxw,minh,maxh=rtk.Container._get_min_max_sizes(self,sw*scale,sh*scale,true,true,scale)return minw,maxw,minh,maxh,sx,sy,sw,sh +end +function rtk.Window:_reflow(boxx,boxy,boxw,boxh,fillw,filly,clampw,clamph,uiscale,viewport,window,greedyw,greedyh)local calc=self.calc +rtk.Container._reflow(self,boxx,boxy,boxw,boxh,fillw,filly,clampw,clamph,uiscale,viewport,window,greedyw,greedyh)calc.x=0 +calc.y=0 +end +function rtk.Window:reflow(mode)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 +if #self.children==0 then +calc.w=self.w and calc.w or calc.minw +calc.h=self.h and calc.h or calc.minh +else +local saved_size +local boxw,boxh=calc.w,calc.h +if not self.w or not self.h or rtk.isrel(self.w)or rtk.isrel(self.h)then +local _,_,sw,sh=self:_get_display_resolution(true,not calc.borderless)boxw=(rtk.isrel(self.w)or not self.w)and sw*rtk.scale.framebuffer or boxw +boxh=(rtk.isrel(self.h)or not self.h)and sh*rtk.scale.framebuffer or boxh +end +local _,_,w,h=rtk.Container.reflow(self,0,0,boxw,boxh,nil,nil,true,true,rtk.scale.value,nil,self,self.w~=nil,self.h~=nil +)self:_realize_geometry()full=true +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()rtk.tick=rtk.tick+1 +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 +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 keycode=gfx.getchar()if keycode>0 then +while keycode>0 do +event=self._event:reset(rtk.Event.KEY)event:set_modifiers(gfx.mouse_cap,0)event:set_keycode(keycode)self:onkeypresspre(event)self:_handle_window_event(event,now)self:onkeypresspost(event)if not event.handled then +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 +keycode=gfx.getchar()end +elseif keycode<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 +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 +need_draw=need_draw or self._draw_queued or in_window_changed +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,nil}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 +local suppress=false +if not event or mouse_moved then +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 +buttonstate.tick=rtk.tick +rtk.mouse.state.order[#rtk.mouse.state.order+1]=event.button +rtk.mouse.state.latest=event.button +elseif event.type==rtk.Event.MOUSEUP then +if rtk.touchscroll and event.buttons==0 and self._restore_mouse_pos then +self._restore_mouse_pos[3]=now+0.2 +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 need_draw or self._draw_queued and not self._sync_window_attrs_on_update then +if self._reflow_queued then +if self:reflow()then +calc.cursor=rtk.mouse.cursors.UNDEFINED +local tmp=event:clone{type=rtk.Event.MOUSEMOVE,simulated=true}self:_handle_window_event(tmp,now)end +end +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 _,info in pairs(rtk._modal)do +local widget,modaltick=table.unpack(info)local state=rtk.mouse.state[event.button] +if not state or(modaltick~=state.modaltick)then +widget:_release_modal(event)end +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.MOUSEUP then +rtk.mouse.last[event.button]={x=event.x,y=event.y}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 +rtk.mouse.state[event.button]=nil +if event.buttons==0 then +rtk._pressed_widgets=nil +end +end +if calc.cursor==rtk.mouse.cursors.UNDEFINED then +calc.cursor=self.cursor +end +if self.in_window and not suppress 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 self._restore_mouse_pos and not buttons_down then +local x,y,when=table.unpack(self._restore_mouse_pos)if when and now>=when then +reaper.JS_Mouse_SetPosition(x,y)self._restore_mouse_pos=nil +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 +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()return sh-self.y-gfx.h/rtk.scale.framebuffer-self._os_window_frame_height +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,greedyw,greedyh)local calc=self.calc +calc.x,calc.y=self:_get_box_pos(boxx,boxy)local w,h,tp,rp,bp,lp,minw,maxw,minh,maxh=self:_get_content_size(boxw,boxh,fillw,fillh,clampw,clamph,nil,greedyw,greedyh +)local inner_maxw=rtk.clamp(w or(boxw-lp-rp),minw,maxw)local inner_maxh=rtk.clamp(h or(boxh-tp-bp),minh,maxh)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,expw,exph,expand_units,remaining_size,total_spacing=self:_reflow_step1(inner_maxw,inner_maxh,clampw,clamph,uiscale,viewport,window,greedyw,greedyh +)if self.orientation==rtk.Box.HORIZONTAL then +expw=(expand_units>0)or expw +elseif self.orientation==rtk.Box.VERTICAL then +exph=(expand_units>0)or exph +end +innerw,innerh=self:_reflow_step2(inner_maxw,inner_maxh,innerw,innerh,clampw,clamph,expand_units,remaining_size,total_spacing,uiscale,viewport,window,greedyw,greedyh,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 greedyw and inner_maxw or 0)innerh=h or math.max(innerh,fillh and greedyh and inner_maxh or 0)calc.w=math.ceil(rtk.clamp(innerw+lp+rp,minw,maxw))calc.h=math.ceil(rtk.clamp(innerh+tp+bp,minh,maxh))return expw,exph +end +function rtk.Box:_reflow_step1(w,h,clampw,clamph,uiscale,viewport,window,greedyw,greedyh)local calc=self.calc +local orientation=calc.orientation +local remaining_size,greedy +if orientation==rtk.Box.HORIZONTAL then +remaining_size=w +greedy=greedyw +else +remaining_size=h +greedy=greedyh +end +local expand_units=0 +local maxw,maxh=0,0 +local spacing=0 +local total_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 +attrs._minw=self:_adjscale(attrs.minw,uiscale,greedyw and w)attrs._maxw=self:_adjscale(attrs.maxw,uiscale,greedyw and w)attrs._minh=self:_adjscale(attrs.minh,uiscale,greedyh and h)attrs._maxh=self:_adjscale(attrs.maxh,uiscale,greedyh and h)local implicit_expand +if orientation==rtk.Box.HORIZONTAL then +implicit_expand=attrs.fillw and greedyw +else +implicit_expand=attrs.fillh and greedyh +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 or not greedy 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.clamp(remaining_size-clp-crp-spacing,attrs._minw,attrs._maxw)local child_maxh=rtk.clamp(h-ctp-cbp,attrs._minh,attrs._maxh)_,_,ww,wh,wexpw,wexph=widget:reflow(0,0,child_maxw,child_maxh,attrs.fillw and greedyw,attrs.fillh and greedyh and attrs.stretch~=rtk.Box.STRETCH_TO_SIBLINGS,clampw,clamph,uiscale,viewport,window,greedyw,greedyh +)expw=wexpw or expw +exph=wexph or exph +ww=math.max(ww,attrs._minw or 0)wh=math.max(wh,attrs._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.clamp(w-clp-crp,attrs._minw,attrs._maxw)local child_maxh=rtk.clamp(remaining_size-ctp-cbp-spacing,attrs._minh,attrs._maxh)_,_,ww,wh,wexpw,wexph=widget:reflow(0,0,child_maxw,child_maxh,attrs.fillw and greedyw and attrs.stretch~=rtk.Box.STRETCH_TO_SIBLINGS,attrs.fillh and greedyh,clampw,clamph,uiscale,viewport,window,greedyw,greedyh +)expw=wexpw or expw +exph=wexph or exph +ww=math.max(ww,attrs._minw or 0)wh=math.max(wh,attrs._minh 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 and greedyw)exph=exph or(attrs.fillh and greedyh)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 and greedyw then +maxw=w +elseif orientation==rtk.Box.HORIZONTAL and attrs.stretch==rtk.Box.STRETCH_FULL and greedyh then +maxh=h +end +attrs._running_spacing_total=spacing +spacing=(attrs.spacing or self.spacing)*rtk.scale.value +total_spacing=total_spacing+spacing +self:_add_reflowed_child(widgetattrs,attrs.z or wcalc.z or 0)else +widget.realized=false +end +end +self:_determine_zorders()return maxw,maxh,expw,exph,expand_units,remaining_size,total_spacing +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_units,remaining_size,total_spacing,uiscale,viewport,window,greedyw,greedyh,tp,rp,bp,lp)local expand_unit_size=expand_units>0 and((remaining_size-total_spacing)/expand_units)or 0 +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 +if greedyh 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)end +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 greedyw)and +attrs.stretch~=rtk.Box.STRETCH_FULL))local offx=lp+clp +local offy=offset+tp+ctp+spacing +local expand=attrs._calculated_expand +local cellh +if expand and greedyh and expand>0 then +local expanded_size=(expand_unit_size*expand)expand_units=expand_units-expand +if attrs._minh and attrs._minh>expanded_size then +local remaining_spacing=total_spacing-attrs._running_spacing_total +expand_unit_size=(remaining_size-attrs._minh-ctp-cbp-remaining_spacing)/expand_units +end +local child_maxw=rtk.clamp(w-clp-crp,attrs._minw,attrs._maxw)local child_maxh=rtk.clamp(expanded_size-ctp-cbp-spacing,attrs._minh,attrs._maxh)child_maxh=math.min(child_maxh,h-maxh-spacing)wx,wy,ww,wh=widget:reflow(0,0,child_maxw,child_maxh,attrs.fillw,attrs.fillh,clampw,clamph,uiscale,viewport,window,greedyw,greedyh +)if attrs.stretch==rtk.Box.STRETCH_FULL and greedyw then +ww=maxw +end +wh=math.max(child_maxh,wh)cellh=ctp+wh+cbp +remaining_size=remaining_size-spacing-cellh +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,cellh)end +else +ww=attrs.stretch==rtk.Box.STRETCH_FULL and greedyw and(maxw-clp-crp)or wcalc.w +wh=math.max(wcalc.h,attrs._minh or 0)cellh=ctp+wh+cbp +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,cellh)end +end +if wcalc.position&rtk.Widget.POSITION_INFLOW~=0 then +offset=offset+spacing+cellh +end +maxw=math.max(maxw,ww+clp+crp)maxh=math.max(maxh,offset)spacing=(attrs.spacing or self.spacing)*uiscale +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 +widget:reflow(0,0,maxw,child_maxh,attrs.fillw,attrs.fillh,clampw,clamph,uiscale,viewport,window,greedyw,greedyh +)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_units,remaining_size,total_spacing,uiscale,viewport,window,greedyw,greedyh,tp,rp,bp,lp)local expand_unit_size=expand_units>0 and((remaining_size-total_spacing)/expand_units)or 0 +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 +if greedyw 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)end +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 greedyh)and +attrs.stretch~=rtk.Box.STRETCH_FULL))local offx=offset+lp+clp+spacing +local offy=tp+ctp +local expand=attrs._calculated_expand +local cellw +if expand and greedyw and expand>0 then +local expanded_size=(expand_unit_size*expand)expand_units=expand_units-expand +if attrs._minw and attrs._minw>expanded_size then +local remaining_spacing=total_spacing-attrs._running_spacing_total +expand_unit_size=(remaining_size-attrs._minw-clp-crp-remaining_spacing)/expand_units +end +local child_maxw=rtk.clamp(expanded_size-clp-crp,attrs._minw,attrs._maxw)child_maxw=math.min(child_maxw,w-maxw-spacing)local child_maxh=rtk.clamp(h-ctp-cbp,attrs._minh,attrs._maxh)wx,wy,ww,wh=widget:reflow(0,0,child_maxw,child_maxh,attrs.fillw,attrs.fillh,clampw,clamph,uiscale,viewport,window,greedyw,greedyh +)if attrs.stretch==rtk.Box.STRETCH_FULL and greedyh then +wh=maxh +end +ww=math.max(child_maxw,ww)cellw=clp+ww+crp +remaining_size=remaining_size-spacing-cellw +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,cellw,wh+ctp+cbp)end +else +ww=math.max(wcalc.w,attrs._minw or 0)wh=attrs.stretch==rtk.Box.STRETCH_FULL and greedyh and(maxh-ctp-cbp)or wcalc.h +cellw=clp+ww+crp +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,cellw,wh+ctp+cbp)end +end +if wcalc.position&rtk.Widget.POSITION_INFLOW~=0 then +offset=offset+spacing+cellw +end +maxw=math.max(maxw,offset)maxh=math.max(maxh,wh+ctp+cbp)spacing=(attrs.spacing or self.spacing)*uiscale +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 +widget:reflow(0,0,child_maxw,maxh,attrs.fillw,attrs.fillh,clampw,clamph,uiscale,viewport,window,greedyw,greedyh +)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,greedyw,greedyh)local calc=self.calc +local x,y=self:_get_box_pos(boxx,boxy)local w,h,tp,rp,bp,lp,minw,maxw,minh,maxh=self:_get_content_size(boxw,boxh,fillw,fillh,clampw,clamph,nil,greedyw,greedyh +)local inner_maxw=rtk.clamp(w or(boxw-lp-rp),minw,maxw)local inner_maxh=rtk.clamp(h or(boxh-tp-bp),minh,maxh)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)attrs._minw=self:_adjscale(attrs.minw,uiscale,greedyw and inner_maxw)attrs._maxw=self:_adjscale(attrs.maxw,uiscale,greedyw and inner_maxw)attrs._minh=self:_adjscale(attrs.minh,uiscale,greedyh and inner_maxh)attrs._maxh=self:_adjscale(attrs.maxh,uiscale,greedyh and inner_maxh)local wx,wy,ww,wh=widget:reflow(0,0,rtk.clamp(inner_maxw,attrs._minw,attrs._maxw),rtk.clamp(inner_maxh,attrs._minh,attrs._maxh),nil,nil,clampw,clamph,uiscale,viewport,window,false,false +)ww=ww+clp+crp +wh=wh+ctp+cbp +child_maxw=math.min(math.max(child_maxw,ww,attrs._minw or 0),inner_maxw)child_totalh=child_totalh+math.max(wh,attrs._minh or 0)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=math.ceil(child_maxw)local num_columns=math.max(1,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=self.parent and self.parent.calc.bg or 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 +if rtk.os.mac and icon then +ly=ly+math.ceil(rtk.scale.value)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 parentbg=self.parent and self.parent.calc.bg +local style=rtk.color.get_icon_style(self.calc.bg,parentbg or 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,greedyw,greedyh)local calc=self.calc +local lmaxw,lmaxh=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')lmaxw,lmaxh=charwidth*calc.textwidth,self._font.texth +else +lmaxw, lmaxh=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,minw,maxw,minh,maxh=self:_get_content_size(boxw,boxh,fillw,fillh,clampw,clamph,nil,greedyw,greedyh +)calc.w=math.ceil(rtk.clamp((w or lmaxw)+lp+rp,minw,maxw))calc.h=math.ceil(rtk.clamp((h or lmaxh)+tp+bp,minh,maxh))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=math.max(0,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._caretctr=0 +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(event)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(event)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 +local last=rtk.mouse.last[event.button] +local dx=last and math.abs(last.x-event.x)or 0 +local dy=last and math.abs(last.y-event.y)or 0 +if dx<3 and dy<3 then +self:select_all()end +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,event)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(event)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(event)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 not self._dirty_text then +gfx.x,gfx.y=x+lp,y+tp +local r,g,b=gfx.getpixel()if self._lastbg_r~=r or self._lastbg_g~=g or self._lastbg_b~=b then +self._lastbg_r,self._lastbg_g,self._lastbg_b=r,g,b +self._dirty_text=true +end +end +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,event)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 +if curx>x and curx<=x+calc.w-rp then +self:setcolor(calc.textcolor,alpha)gfx.line(curx,y+tp,curx,y+calc.h-bp,0)end +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{reflow=rtk.Widget.REFLOW_NONE,default=rtk.Attribute.NIL,calculate=function(self,attr,value,target)if not value then +local parentbg=self.parent and self.parent.calc.bg +local luma=rtk.color.luma(self.calc.bg,parentbg or rtk.theme.bg)value=rtk.themes[luma > rtk.light_luma_threshold and 'light' or 'dark'].text +end +return {rtk.color.rgba(value)}end,},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()self._num_newlines=nil +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 not self.calc.wrap then +if self.w or(self.box and self.box[5])then +local c=value:count('\n')if c==self._num_newlines then +reflow=rtk.Widget.REFLOW_PARTIAL +end +self._num_newlines=c +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 +elseif attr=='bg' and not self.color then +self:attr('color', self.color, true, rtk.Widget.REFLOW_NONE)end +return ok +end +function rtk.Text:_reflow(boxx,boxy,boxw,boxh,fillw,fillh,clampw,clamph,uiscale,viewport,window,greedyw,greedyh)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,minw,maxw,minh,maxh=self:_get_content_size(boxw,boxh,fillw,fillh,clampw,clamph,nil,greedyw,greedyh +)local hpadding=lp+rp +local vpadding=tp+bp +local lmaxw=w or((clampw or(fillw and greedyw))and(boxw-hpadding)or math.inf)local lmaxh=h or((clamph or(fillh and greedyh))and(boxh-vpadding)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 greedyw and boxw)or math.min(clampw and boxw or math.inf,self.lw+hpadding)calc.h=(h and h+vpadding)or(fillh and greedyh and boxh)or math.min(clamph and boxh or math.inf,self.lh+vpadding)calc.w=math.ceil(rtk.clamp(calc.w,minw,maxw))calc.h=math.ceil(rtk.clamp(calc.h,minh,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{default=rtk.Attribute.NIL,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 +elseif attr == 'bg' and type(self.image) == 'string' then +self:attr('image', self.image, true, rtk.Widget.REFLOW_NONE)end +return ret +end +function rtk.ImageBox:_reflow(boxx,boxy,boxw,boxh,fillw,fillh,clampw,clamph,uiscale,viewport,window,greedyw,greedyh)local calc=self.calc +calc.x,calc.y=self:_get_box_pos(boxx,boxy)local w,h,tp,rp,bp,lp,minw,maxw,minh,maxh=self:_get_content_size(boxw,boxh,fillw,fillh,clampw,clamph,self.scale or 1,greedyw,greedyh +)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)*uiscale/image.density +local native_aspect=image.w/image.h +local aspect=calc.aspect or native_aspect +dstw=(w and w-hpadding)or((fillw and greedyw)and boxw-hpadding)dsth=(h and h-vpadding)or((fillh and greedyh)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 greedyw and boxw)or math.min(clampw and boxw or math.inf,self.iw+hpadding)calc.h=(fillh and greedyh and boxh)or math.min(clamph and boxh or math.inf,self.ih+vpadding)calc.w=math.ceil(rtk.clamp(calc.w,minw,maxw))calc.h=math.ceil(rtk.clamp(calc.h,minh,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 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)if calc.image then +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,}end +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,['true']=rtk.CheckBox.static.CHECKED,checked=rtk.CheckBox.static.CHECKED,['false']=rtk.CheckBox.static.UNCHECKED,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=""}, {fillw=true})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 ' ')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,greedyw,greedyh)local calc=self.calc +calc.x,calc.y=self:_get_box_pos(boxx,boxy)local w,h,tp,rp,bp,lp,minw,maxw,minh,maxh=self:_get_content_size(boxw,boxh,fillw,fillh,clampw,clamph,nil,greedyw,greedyh +)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 greedyw))and(boxw-hpadding)or w or math.inf +local lmaxh=(clamph or(fillh and greedyh))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 +minw=math.max(minw or 0,math.max(calc.minw or 0,#calc.value*calc.thumbsize*2)*rtk.scale.value)minh=math.max(minh or 0,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(greedyw and boxw or 50)calc.h=h and(h+vpadding)or(size+self.lh+vpadding)calc.w=math.ceil(rtk.clamp(calc.w,minw,maxw))calc.h=math.ceil(rtk.clamp(calc.h,minh,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(event)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)() + +__mod_rtk_xml=(function() +local rtk=__mod_rtk_core +local log=__mod_rtk_log +local ATTR_PATTERNS={{'quoted', '^%s*([^>/%s=]+)%s*(=)%s*"([^"]+)"%s*(%/?)(%>?)'},{'quoted', "^%s*([^>/%s=]+)%s*(=)%s*'([^']+)'%s*(%/?)(%>?)"},{'mustache', '^%s*([^>/%s=]+)%s*(=)%s*({{)'},{'unquoted', '^%s*([^>/%s=]+)%s*(=)%s*([^%s/>]+)%s*(%/)(%>)'},{'unquoted', '^%s*([^>/%s=]+)%s*(=)%s*([^%s>]+)(%s*)(%>?)'},{'novalue', '^%s*([^>/%s]+)%s*()()(%/?)(%>?)'},}local ENTITIES={lt='<',gt='>',amp='&',apos="'",quot='"',nbsp=" ",}local function _unescape_entity(entity)local r=ENTITIES[entity] +if not r and entity:sub(1, 1)=='#' then +if entity:sub(2, 2)=='x' then +r=utf8.char(tonumber(entity:sub(3),16))else +r=utf8.char(tonumber(entity:sub(2)))end +end +return r +end +local function _unescape(s)return s and s:gsub('&([^;]+);', _unescape_entity)end +local function _gettag(s,pos,elem,userdata,ontagstart,onattr)local a, b, preamble, close, tag, selfclose, term=s:find('^([^%<]*)%<%s*(%/?)%s*([^>/%s]+)%s*(%/?)%s*(%>?)', pos)if not a then +return +end +pos=b+1 +preamble=preamble:strip()if tag=='!--' then +local finish=s:find('%-%->', pos)if finish then +return finish+3,nil,false +else +return +end +elseif tag=='!DOCTYPE' then +local finish=s:find(']>', pos)if finish then +return finish+2,nil,false +else +log.warning('rtk.xml: invalid XML: DOCTYPE is not terminated')end +elseif tag:sub(1, 8)=='![CDATA[' then +local finish=s:find(']]>', pos)if finish then +if elem then +elem.content=tag:sub(9)..s:sub(pos,finish-1)else +log.warning('rtk.xml: invalid XML: CDATA occurs outside an element')end +return finish+3,nil,false +else +log.warning('rtk.xml: invalid XML: unterminated CDATA')if elem then +elem.content=tag:sub(9)..s:sub(pos)end +return +end +end +if close=='/' then +if not elem then +log.warning('rtk.xml: invalid XML: unexpected end tag "%s"', tag)return +elseif elem.tag~=tag then +log.warning('rtk.xml: mismatched end tag "%s" -- expected "%s"', tag, elem.tag)return +end +if preamble ~="" then +elem.content=(elem.content or '') .. _unescape(preamble)end +return pos, elem, close=='/' and 1 +elseif preamble ~="" and elem then +elem.preamble=preamble +end +elem={tag=tag}if ontagstart and tag ~='?xml' then +ontagstart(elem,userdata)end +if term=='>' then +return pos,elem,false +end +local attrs={}elem.attrs=attrs +local attr,eq,value,whitespace +while true do +local pattern_type=nil +for p=1,#ATTR_PATTERNS do +local typ,pattern=table.unpack(ATTR_PATTERNS[p])a,b,attr,eq,value,selfclose,term=s:find(pattern,pos)if a then +pattern_type=typ +break +end +end +if not pattern_type or b+1<=pos then +break +end +if pattern_type=='mustache' then +local finish=s:find('}}', b+1)if not finish then +error(string.format('rtk.xml: terminating }} for expression not found for "%s"', attr), 3)end +value=s:sub(b+1,finish-1)b=finish+2 +a, b, whitespace, selfclose, term=s:find('^(%s*)(%/?)(%>?)', b)if #selfclose==0 and #term==0 and #whitespace>0 then +error('rtk.xml: mustache expression has trailing characters -- perhaps quotes are needed?', 3)end +elseif pattern_type=='novalue' then +value=nil +else +value=_unescape(value)end +local attrtable={name=attr,value=value,type=pattern_type}if onattr and tag ~='?xml' then +onattr(elem,attrtable,userdata)end +assert(attrtable.name, 'attribute is missing name')attrs[attrtable.name]=attrtable +pos=b+1 +if term=='>' then +break +end +end +return pos, elem, selfclose=='/' and 2 +end +function rtk.xmlparse(args)local xml,userdata,ontagstart,ontagend,onattr +if type(args)=='string' then +xml=args +elseif type(args)=='table' then +xml=args.xml or args[1] +userdata=args.userdata or args[2] +ontagstart=args.ontagstart +ontagend=args.ontagend +onattr=args.onattr +else +error('rtk.xmlparse() must receive either a string or table')end +assert(type(xml)=='string', 'the XML document must be a string')local stack={}local root=nil +local pos=1 +while true do +local last=stack[#stack] +local newpos,elem,closed=_gettag(xml,pos,last,userdata,ontagstart,onattr)if not newpos or newpos<=pos then +break +end +pos=newpos +if closed and ontagend then +ontagend(elem,userdata)end +if closed==1 then +table.remove(stack,#stack)elseif elem and elem.tag ~='?xml' then +if #stack>0 then +local current=stack[#stack] +current[#current+1]=elem +end +if not closed then +stack[#stack+1]=elem +end +end +if not root then +root=stack[#stack] +end +end +return root +end +end)() + +rtk.log=__mod_rtk_log +local function init()rtk.script_path=({reaper.get_action_context()})[2]:match('^.+[\\//]')rtk._image_paths.fallback={rtk.script_path}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