-------------------------------------------------------- -- Minetest :: Scarlet Mod (scarlet) -- -- See README.txt for licensing and release notes. -- Copyright (c) 2019, Leslie E. Krause -------------------------------------------------------- scarlet = { } -------------------------- -- Local Helper Functions -------------------------- local function element( name, params ) return name .. "[" .. table.concat( params, ";" ) .. "]" end local function is_match( text, glob ) _ = { string.match( text, glob ) } return #_ > 0 end ------------------------ -- UnitConversion Class ------------------------ function UnitConversion( screen_dpi, gui_scaling, has_padding ) local self = { } local image_size = 0.55 * ( screen_dpi or 72 ) * ( gui_scaling or 1 ) -- calculate the dot pitch as images per pixel and cells per pixel -- using the "black magic" algorithm from guiFormSpecMenu.cpp self.dot_pitch = { -- imgsize (images per pixel) ix = 1 / image_size, iy = 1 / image_size, -- spacing (cells per pixel) x = 1 / image_size * 4 / 5, y = 1 / image_size * 13 / 15 } -- padding width and height in cell units -- original formula: image_size * 3.0 / 8 self.padding_width = has_padding and 0.0 or 3 / 10 self.padding_height = has_padding and 0.0 or 13 / 40 -- button height in cell units -- NB: this is 2x the value of m_btn_height used internally! -- original formula: image_size * 15.0 / 13 * 0.35 self.button_height = 0.7 -- cell margin width and height in cell units -- original formula: vx * spacing.x - ( spacing.x - imgsize.x ) self.cell_margin_width = 1 / 5 self.cell_margin_height = 2 / 15 -- point width and height in cell units self.point_width = 1 / 39.6 * 4 / 5 self.point_height = 1 / 39.6 * 13 / 15 self.units = ( function ( ) -- cell size measurements local factors = { p = { x = self.point_width, y = self.point_height }, i = { x = 4 / 5, y = 13 / 15 }, -- imgsize c = { x = 1, y = 1 }, -- spacing (unity) b = { y = self.button_height }, -- 2 x m_btn_height } local function get_x( v, u, dot_pitch ) return not factors[ u ] and math.floor( v ) * self.dot_pitch.x or v * factors[ u ].x end local function get_y( v, u, dot_pitch ) return not factors[ u ] and math.floor( v ) * self.dot_pitch.y or v * factors[ u ].y end return { get_x = get_x, get_y = get_y } end )( ) self.iunits = ( function ( ) -- image size measurements local factors = { p = { x = 1 / 39.6, y = 1 / 39.6 }, i = { x = 1, y = 1 }, -- imgsize (unity) c = { x = 5 / 4, y = 15 / 13 }, -- spacing b = { y = self.button_height * 15 / 13 }, -- 2 x m_btn_height } local function get_x( v, u ) return not factors[ u ] and math.floor( v ) * self.dot_pitch.ix or v * factors[ u ].x end local function get_y( v, u ) return not factors[ u ] and math.floor( v ) * self.dot_pitch.iy or v * factors[ u ].y end return { get_x = get_x, get_y = get_y } end )( ) self.evaluate = function ( axis, expr, old_res ) local res = 0.0 local glob, vars, to_val if axis == "x" then glob = "^([-+][0-9.]+)([cpdi])$" vars = { P = self.padding_width, S = self.cell_margin_width, R = old_res or 0.0 } to_val = self.units.get_x elseif axis == "y" then glob = "^([-+][0-9.]+)([cpdib])$" vars = { P = self.padding_height, S = self.cell_margin_height, B = self.button_height, R = old_res or 0.0 } to_val = self.units.get_y else return end local pos1 = 1 if not string.match( expr, "^[-+]" ) then expr = "+" .. expr end while pos1 do local pos2 = string.find( expr, "[-+]", pos1 + 1 ) local token = string.sub( expr, pos1, pos2 and pos2 - 1 ) if is_match( token, "^([-+])%$([A-Z])$" ) and vars[ _[ 2 ] ] then res = res + tonumber( _[ 1 ] .. vars[ _[ 2 ] ] ) elseif is_match( token, glob ) then res = res + to_val( tonumber( _[ 1 ] ), _[ 2 ] ) else return end pos1 = pos2 end return res end return self end --------------------------- -- RuntimeTranslator Class --------------------------- function RuntimeTranslator( screen_dpi, gui_scaling, has_padding ) local self = UnitConversion( screen_dpi, gui_scaling, has_padding ) -- extend the base class self.margins = { { width = 0.0, height = 0.0 } } self.insert_margin = function ( width, height ) table.insert( self.margins, 1, { width = width + self.margins[ 1 ].width, height = height + self.margins[ 1 ].height } ) end self.remove_margin = function ( ) if #self.margins > 1 then table.remove( self.margins, 1 ) end end self.get_pos = function ( pos_value, pos_units, offsets ) if pos_value then local pos_x, u1, pos_y, u2 = string.match( pos_value, "^(-?[0-9.]+)([icpd]?),(-?[0-9.]+)([icpdb]?)$" ) if not pos_x then return end return string.format( "%0.3f,%0.3f", pos_units.get_x( pos_x, u1 ) + offsets[ 1 ] + self.margins[ 1 ].width, pos_units.get_y( pos_y, u2 ) + offsets[ 2 ] + self.margins[ 1 ].height ) end end self.get_pos_and_dim_x = function ( pos_value, dim_value, pos_units, dim_units, offsets ) if pos_value and dim_value then local pos_x, u1, pos_y, u2 = string.match( pos_value, "^(-?[0-9.]+)([icpd]?),(-?[0-9.]+)([icpdb]?)$" ) local dim_x, u3 = string.match( dim_value, "^([0-9.]+)([iscp]?)$" ) if not pos_x or not dim_x then return end if u3 == "s" then u3 = "i" offsets[ 3 ] = offsets[ 3 ] + ( dim_x % 1 == 0 and dim_x - 1 or math.floor( dim_x ) ) * self.cell_margin_width end return string.format( "%0.3f,%0.3f;%0.3f,0.000", pos_units.get_x( pos_x, u1 ) + offsets[ 1 ] + self.margins[ 1 ].width, pos_units.get_y( pos_y, u2 ) + offsets[ 2 ] + self.margins[ 1 ].height, dim_units.get_x( dim_x, u3 ) + offsets[ 3 ] ) end end self.get_pos_and_dim = function ( pos_value, dim_value, pos_units, dim_units, offsets ) if pos_value and dim_value then local pos_x, u1, pos_y, u2 = string.match( pos_value, "^(-?[0-9.]+)([icpd]?),(-?[0-9.]+)([icpdb]?)$" ) local dim_x, u3, dim_y, u4 = string.match( dim_value, "^([0-9.]+)([iscpd]?),([0-9.]+)([iscpdb]?)$" ) if not pos_x or not dim_x then return end if u3 == "s" then u3 = "i" offsets[ 3 ] = offsets[ 3 ] + ( dim_x % 1 == 0 and dim_x - 1 or math.floor( dim_x ) ) * self.cell_margin_width end if u4 == "s" then u4 = "i" offsets[ 4 ] = offsets[ 4 ] + ( dim_y % 1 == 0 and dim_y - 1 or math.floor( dim_y ) ) * self.cell_margin_height end return string.format( "%0.3f,%0.3f;%0.3f,%0.3f", pos_units.get_x( pos_x, u1 ) + offsets[ 1 ] + self.margins[ 1 ].width, pos_units.get_y( pos_y, u2 ) + offsets[ 2 ] + self.margins[ 1 ].height, dim_units.get_x( dim_x, u3 ) + offsets[ 3 ], dim_units.get_y( dim_y, u4 ) + offsets[ 4 ] ) end end self.get_pos_raw = function ( pos_value ) if pos_value then return string.match( pos_value, "^(-?[0-9.]+)([icpd]?),(-?[0-9.]+)([icpdb]?)$" ) end end self.get_dim_raw = function ( dim_value ) if dim_value then return string.match( dim_value, "^([0-9.]+)([iscpd]?),([0-9.]+)([iscpdb]?)$" ) end end return self end ------------------------------ -- Generic Element Subclasses ------------------------------ local function ElemPos( pos_units, length, has_padding ) return function ( tx, name, params ) local pos = tx.get_pos( params[ 1 ], pos_units, { has_padding and -tx.padding_width or 0, has_padding and -tx.padding_height or 0 } ) assert( pos and #params == length, "Cannot parse formspec element " .. name .. "[]" ) return element( name, { pos, unpack( params, 2 ) } ) end end local function ElemPosAndDim( pos_units, dim_units, length, has_padding ) return function ( tx, name, params ) local pos_and_dim = tx.get_pos_and_dim( params[ 1 ], params[ 2 ], pos_units, dim_units, { has_padding and -tx.padding_width or 0, has_padding and -tx.padding_height or 0, 0, 0 } ) assert( pos_and_dim and #params == length, "Cannot parse formspec element " .. name .. "[]" ) return element( name, { pos_and_dim, unpack( params, 3 ) } ) end end local function ElemPosAndDimX( pos_units, dim_units, length, has_padding ) return function ( tx, name, params ) local pos_and_dim_x = tx.get_pos_and_dim_x( params[ 1 ], params[ 2 ], pos_units, dim_units, { has_padding and -tx.padding_width or 0, has_padding and -tx.padding_height or 0, 0 } ) assert( pos_and_dim_x and #params == length, "Cannot parse formspec element " .. name .. "[]" ) return element( name, { pos_and_dim_x, unpack( params, 3 ) } ) end end ------------------------------ -- Special Element Subclasses ------------------------------ -- size[,] -- size[,;,] local function SizeElement( ) return function ( tx, name, params ) local pos_offset_x, pos_offset_y local dim_x, u1, dim_y, u2 = tx.get_dim_raw( params[ 1 ] ) local pos_x, u3, pos_y, u4 = tx.get_pos_raw( params[ 2 ] ) assert( #params == 1 and dim_x or #params == 2 and dim_x and ( params[ 2 ] == "" or pos_x ), "Cannot parse formspec element size[]" ) if u1 == "s" then u1 = "c" dim_x = dim_x - tx.cell_margin_width end if u2 == "s" then u2 = "c" dim_y = dim_y - tx.cell_margin_height end if not pos_x then pos_offset_x = tx.padding_width pos_offset_y = tx.padding_height else pos_offset_x = tx.units.get_x( pos_x, u3 ) pos_offset_y = tx.units.get_y( pos_y, u4 ) end tx.insert_margin( pos_offset_x, pos_offset_y ) -- original formulas: -- padding.x * 2 + spacing.x * ( vx - 1.0 ) + imgsize.x -- padding.y * 2 + spacing.y * ( vy - 1.0 ) + imgsize.y + m_btn_height * 2.0 / 3.0 local dim = string.format( "%0.3f,%0.3f", tx.units.get_x( dim_x, u1 ) + 2 * pos_offset_x + 1 - tx.padding_width * 2 - 4 / 5, tx.units.get_y( dim_y, u2 ) + 2 * pos_offset_y + 1 - tx.padding_height * 2 - 13 / 15 - tx.button_height / 3 ) return element( "size", { dim } ) end end -- list[;;,;;] local function ListElement( ) local pattern = "^(-?[0-9.]+)([icpd]?),(-?[0-9.]+)([icpdb]?)$" local replace = "%0.3f,%0.3f" return function ( tx, name, params ) local pos = tx.get_pos( params[ 3 ], tx.units, { -tx.padding_width, -tx.padding_height, } ) assert( pos and ( #params == 4 or #params == 5 ), "Cannot parse formspec element list[]" ) return element( "list", { params[ 1 ], params[ 2 ], pos, unpack( params, 4 ) } ) end end -- margin[] -- margin[,] local function MarginElement( ) local pattern = "^(-?[0-9.]+)([icpd]?),(-?[0-9.]+)([icpdb]?)$" return function ( tx, name, params ) if #params == 0 then tx.insert_margin( tx.padding_width, tx.padding_height ) else local pos_x, u1, pos_y, u2 = string.match( params[ 1 ], pattern ) assert( pos_x and #params == 1, "Cannot parse formspec element margin[]" ) tx.insert_margin( tx.units.get_x( pos_x, u1 ), tx.units.get_y( pos_y, u2 ) ) end return "" -- remove virtual element end end -- margin_end[] local function MarginEndElement( ) return function ( tx, name, params ) assert( #params == 0, "Cannot parse formspec element container_end[]" ) tx.remove_margin( ) return "" -- remove virtual element end end -- background[,;] local function BackgroundElement( ) local pattern = "^(-?%d+),(-?%d+)$" local replace = "%d,%d;0,0" return function ( tx, name, params ) local dim, count = string.gsub( params[ 1 ], pattern, function ( pos_x, pos_y ) return string.format( replace, -pos_x, -pos_y ) end ) assert( count == 1, "Cannot parse formspec element background[]" ) return element( "background", { dim, params[ 2 ], "true" } ) end end -- bgimage[,;,;] local function BgImageElement( ) return function ( tx, name, params ) -- original formula: vx * spacing.x - ( spacing.x - imgsize.x ) / 2 local pos_offset_x = 0.5 * 1 / 5 local pos_offset_y = 0.5 * 2 / 15 local pos_and_dim = tx.get_pos_and_dim( params[ 1 ], params[ 2 ], tx.units, tx.units, { -tx.padding_width + pos_offset_x, -tx.padding_height + pos_offset_y, 0, 0 } ) assert( pos_and_dim and #params == 3, "Cannot parse formspec element bgimage[]" ) return element( "background", { pos_and_dim, params[ 3 ], "false" } ) end end -- label[,;