|
local UIL = UIL |
|
|
|
DefineClass.XImage = { |
|
__parents = { "XControl" }, |
|
|
|
properties = { |
|
{ category = "Image", id = "Image", editor = "ui_image", default = "", }, |
|
{ category = "Image", id = "ImageFit", name = "Fit", editor = "choice", default = "none", items = {"none", "width", "height", "smallest", "largest", "stretch", "stretch-x", "stretch-y", "scale-down"}, invalidate = "measure", }, |
|
{ category = "Image", id = "Rows", editor = "number", default = 1, }, |
|
{ category = "Image", id = "Columns", editor = "number", default = 1, }, |
|
{ category = "Image", id = "Row", editor = "number", default = 1, }, |
|
{ category = "Image", id = "Column", editor = "number", default = 1, }, |
|
{ category = "Image", id = "ImageRect", name = "Custom rect", editor = "rect", default = box(0, 0, 0, 0), help = "Overrides the columns/rows and allows defining a custom rect from the image", }, |
|
{ category = "Image", id = "ImageScale", name = "Scale", editor = "point2d", default = point(1000, 1000), help = "Used when the image is not resized (ImageFit equals 'none')", invalidate = true, lock_ratio = true, }, |
|
{ category = "Image", id = "ImageColor", name = "Image color", editor = "color", default = RGB(255, 255, 255), invalidate = true, }, |
|
{ category = "Image", id = "DisabledImageColor", name = "Disabled image color", editor = "color", default = RGBA(255, 255, 255, 160), invalidate = true, }, |
|
{ category = "Image", id = "Desaturation", editor = "number", default = 0, min = 0, max = 255, slider = true, invalidate = true, }, |
|
{ category = "Image", id = "DisabledDesaturation", editor = "number", default = 255, min = 0, max = 255, slider = true, invalidate = true, }, |
|
{ category = "Image", id = "Angle", editor = "number", default = 0, min = 0, max = 360*60 - 1, slider = true, scale = "deg", invalidate = true, }, |
|
{ category = "Image", id = "AdditiveMode", editor = "bool", default = false, invalidate = true, }, |
|
{ category = "Image", id = "FlipX", editor = "bool", default = false, invalidate = true, }, |
|
{ category = "Image", id = "FlipY", editor = "bool", default = false, invalidate = true, }, |
|
{ category = "Image", id = "EffectPixels", editor = "number", default = 0, invalidate = true, min = 0, max = 32, slider = true, }, |
|
{ category = "Image", id = "EffectColor", editor = "color", default = RGB(255, 255, 255), invalidate = true }, |
|
{ category = "Image", id = "EffectType", editor = "choice", default = "none", items = {"none", "glow", "outline" }, invalidate = true }, |
|
{ category = "Image", id = "BaseColorMap", editor = "bool", default = false, help = "Use to display the base color map of a material in the UI", invalidate = true, }, |
|
|
|
{ category = "Image", id = "FrameEdgeColor", editor = "color", default = RGBA(0,0,0,0), invalidate = true, }, |
|
{ category = "Image", id = "FrameLeft", editor = "number", default = 0, invalidate = true, }, |
|
{ category = "Image", id = "FrameTop", editor = "number", default = 0, invalidate = true, }, |
|
{ category = "Image", id = "FrameRight", editor = "number", default = 0, invalidate = true, }, |
|
{ category = "Image", id = "FrameBottom", editor = "number", default = 0, invalidate = true, }, |
|
|
|
{ category = "Animation", id = "Animate", editor = "bool", default = false, invalidate = true, }, |
|
{ category = "Animation", id = "FPS", editor = "number", default = 10, invalidate = true, }, |
|
{ category = "Animation", id = "AnimFlags", name = "Flags", editor = "set", default = set("looping"), items = { "looping", "ping-pong", "inverse", "back to start" }, }, |
|
{ category = "Animation", id = "AnimDuration", name = "Duration", editor = "number", default = 0 }, |
|
}, |
|
|
|
HandleMouse = false, |
|
animation = false, |
|
src_rect = false, |
|
image_id = const.InvalidResourceID, |
|
image_obj = false, |
|
} |
|
|
|
function XImage:Init() |
|
self:UpdateModifiers() |
|
self:SetImage(self.Image, true) |
|
end |
|
|
|
function XImage:Done() |
|
if self.image_obj ~= false then |
|
self.image_obj:ReleaseRef() |
|
self.image_obj = false |
|
end |
|
end |
|
|
|
function XImage:SetImage(image, force) |
|
if self.Image == (image or "") and not force then return end |
|
self.Image = image or nil |
|
|
|
self:DeleteThread("LoadImage") |
|
if (self.Image or "") == "" then |
|
return |
|
end |
|
|
|
if self.image_obj ~= false then |
|
self.image_obj:ReleaseRef() |
|
self.image_obj = false |
|
end |
|
|
|
self.image_id = ResourceManager.GetResourceID(self.Image) |
|
if self.image_id == const.InvalidResourceID then |
|
printf("once", "Could not load image %s!", self.Image or "") |
|
return |
|
end |
|
|
|
self.image_obj = ResourceManager.GetResource(self.image_id) |
|
if self.image_obj then |
|
local old_rect = self.src_rect |
|
self.src_rect = false |
|
if self:CalcSrcRect() ~= old_rect then |
|
self:InvalidateMeasure() |
|
end |
|
self:Invalidate() |
|
else |
|
self:CreateThread("LoadImage", function(self) |
|
self.image_obj = AsyncGetResource(self.image_id) |
|
local old_rect = self.src_rect |
|
self.src_rect = false |
|
if self:CalcSrcRect() ~= old_rect then |
|
self:InvalidateMeasure() |
|
end |
|
self:Invalidate() |
|
end, self) |
|
end |
|
end |
|
|
|
function XImage:SetRows(rows) |
|
if self.Rows == rows then return end |
|
self.Rows = rows |
|
self.src_rect = false |
|
self:InvalidateMeasure() |
|
self:Invalidate() |
|
end |
|
|
|
function XImage:SetColumns(columns) |
|
if self.Columns == columns then return end |
|
self.Columns = columns |
|
self.src_rect = false |
|
self:InvalidateMeasure() |
|
self:Invalidate() |
|
end |
|
|
|
function XImage:SetRow(row) |
|
if self.Row == row then return end |
|
self.Row = row |
|
self.src_rect = false |
|
self:Invalidate() |
|
end |
|
|
|
function XImage:SetColumn(column) |
|
if self.Column == column then return end |
|
self.Column = column |
|
self.src_rect = false |
|
self:Invalidate() |
|
end |
|
|
|
local anim_flags_set = { |
|
["looping"] = const.intfLooping, |
|
["ping-pong"] = const.intfPingPong, |
|
["inverse"] = const.intfInverse, |
|
["back to start"] = const.intfBackToStart, |
|
} |
|
|
|
function AnimFlagsSetToInt(st) |
|
local flags = 0 |
|
for k,v in pairs(anim_flags_set) do |
|
if st[k] then |
|
flags = flags | v |
|
end |
|
end |
|
return flags |
|
end |
|
|
|
function XImage:SetAnimate(b) |
|
if self.Animate == b then return end |
|
self.Animate = b |
|
if b then |
|
self.animation = { |
|
modifier_type = const.modInterpolation, |
|
type = const.intAnimate, |
|
start = GetPreciseTicks(), |
|
fps = self.FPS, |
|
columns = self.Columns, |
|
rows = self.Rows, |
|
flags = AnimFlagsSetToInt(self.AnimFlags), |
|
duration = 1000, |
|
easing = "Linear", |
|
} |
|
else |
|
self.animation = false |
|
end |
|
self:Invalidate() |
|
end |
|
|
|
function XImage:SetAnimFlags(f) |
|
if self.AnimFlags == f then return end |
|
self.AnimFlags = f |
|
if self.animation then |
|
self.animation.flags = AnimFlagsSetToInt(f) |
|
end |
|
self:Invalidate() |
|
end |
|
|
|
function XImage:SetFPS(fps) |
|
if self.FPS == fps then return end |
|
self.FPS = fps |
|
if self.animation then |
|
self.animation.fps = self.FPS |
|
if self.FPS > 0 then |
|
self.animation.duration = 1000 |
|
end |
|
end |
|
self:Invalidate() |
|
end |
|
|
|
function XImage:SetAnimDuration(duration) |
|
if self.AnimDuration == duration then return end |
|
self.AnimDuration = duration |
|
if self.animation then |
|
self.animation.duration = self.AnimDuration |
|
if self.AnimDuration > 0 then |
|
self.animation.fps = 0 |
|
end |
|
end |
|
self:Invalidate() |
|
end |
|
|
|
function XImage:SetImageRect(rect) |
|
if self.ImageRect == rect then return end |
|
self.ImageRect = rect |
|
self.src_rect = false |
|
self:Invalidate() |
|
end |
|
|
|
function XImage:CalcSrcRect() |
|
local rect = self.src_rect |
|
if not rect then |
|
rect = self.ImageRect |
|
if rect:IsEmpty() then |
|
if self.image_id ~= const.InvalidResourceID then |
|
local w, h = ResourceManager.GetMetadataTextureSizeXY(self.image_id) |
|
w = w / self.Columns |
|
h = h / self.Rows |
|
local column = Clamp(self.Column, 1, self.Columns) - 1 |
|
local row = Clamp(self.Row, 1, self.Rows) - 1 |
|
rect = sizebox(w * column, h * row, w, h) |
|
else |
|
rect = box(0, 0, 0, 0) |
|
end |
|
end |
|
self.src_rect = rect |
|
end |
|
return rect |
|
end |
|
|
|
function XImage:CalcDesaturation() |
|
return self:GetEnabled() and self.Desaturation or self.DisabledDesaturation |
|
end |
|
|
|
local function FitImage(max_width, max_height, width, height, fit) |
|
if width == 0 or height == 0 then return 0, 0 end |
|
if fit == "smallest" or fit == "largest" then |
|
local image_is_wider = width * max_height >= max_width * height |
|
fit = image_is_wider == (fit == "smallest") and "width" or "height" |
|
end |
|
if fit == "width" then |
|
return max_width, MulDivRound(height, max_width, width) |
|
elseif fit == "height" then |
|
return MulDivRound(width, max_height, height), max_height |
|
elseif fit == "stretch" then |
|
return max_width, max_height |
|
elseif fit == "stretch-x" then |
|
return max_width, height |
|
elseif fit == "stretch-y" then |
|
return width, max_height |
|
elseif fit == "scale-down" then |
|
local scale = Min(1000, Min(MulDivRound(max_width, 1000, width), MulDivRound(max_height, 1000, height))) |
|
return MulDivRound(width, scale, 1000), MulDivRound(height, scale, 1000) |
|
else |
|
return width, height |
|
end |
|
end |
|
|
|
function XImage:Measure(preferred_width, preferred_height) |
|
if self.ImageFit == "stretch" then |
|
return XControl.Measure(self, preferred_width, preferred_height) |
|
end |
|
local image_width, image_height = ScaleXY(self.scale, ScaleXY(self.ImageScale, self:CalcSrcRect():sizexyz())) |
|
if self.Angle == 90*60 or self.Angle == 270*60 then |
|
image_width, image_height = image_height, image_width |
|
end |
|
image_width, image_height = FitImage(preferred_width, preferred_height, image_width, image_height, self.ImageFit) |
|
local width, height = XControl.Measure(self, preferred_width, preferred_height) |
|
local fit = self.ImageFit |
|
if fit ~= "stretch" and fit ~= "stretch-x" then |
|
width = Max(image_width, width) |
|
end |
|
if fit ~= "stretch" and fit ~= "stretch-y" then |
|
height = Max(image_height, height) |
|
end |
|
return width, height |
|
end |
|
|
|
function XImage:UpdateModifiers() |
|
self:RemoveModifiers(const.modShader) |
|
if self.BaseColorMap then |
|
self:AddShaderModifier({ |
|
modifier_type = const.modShader, |
|
shader_flags = const.modIgnoreAlpha, |
|
}) |
|
end |
|
end |
|
|
|
function XImage:SetBaseColorMap(value) |
|
if self.BaseColorMap ~= value then |
|
self.BaseColorMap = value |
|
self:UpdateModifiers() |
|
self:Invalidate() |
|
end |
|
end |
|
|
|
function XImage:DrawContent() |
|
if self.Image == "" then return end |
|
if self.AdditiveMode then |
|
UIL.SetBlendMode("blendAdditive") |
|
end |
|
local src = self:CalcSrcRect() |
|
local width, height = ScaleXY(self.scale, ScaleXY(self.ImageScale, src:sizexyz())) |
|
local b = self.content_box |
|
width, height = FitImage(b:sizex(), b:sizey(), width, height, self.ImageFit) |
|
local color = self:GetEnabled() and self.ImageColor or self.DisabledImageColor |
|
if self.Animate then |
|
local old_top = UIL.PushModifier(self.animation) |
|
UIL.DrawXImage(self.Image, |
|
b, width, height, box(0, 0, src:maxx() * self.Columns, src:maxy() * self.Rows), |
|
color, color, color, color, |
|
self:CalcDesaturation(), self.Angle, self.FlipX, self.FlipY, |
|
self.EffectType, self.EffectPixels, self.EffectColor, self.UseClipBox, |
|
self.FrameEdgeColor, self.FrameLeft, self.FrameTop, self.FrameRight, self.FrameBottom) |
|
UIL.ModifiersSetTop(old_top) |
|
else |
|
UIL.DrawXImage(self.Image, |
|
b, width, height, src, |
|
color, color, color, color, |
|
self:CalcDesaturation(), self.Angle, self.FlipX, self.FlipY, |
|
self.EffectType, self.EffectPixels, self.EffectColor, self.UseClipBox, |
|
self.FrameEdgeColor, self.FrameLeft, self.FrameTop, self.FrameRight, self.FrameBottom) |
|
end |
|
if self.AdditiveMode then |
|
UIL.SetBlendMode("blendNormal") |
|
end |
|
end |
|
|
|
|
|
|
|
DefineClass.XEmbedIcon = { |
|
__parents = { "XWindow" }, |
|
properties = { |
|
{ category = "Icon", id = "Icon", editor = "ui_image", default = "", }, |
|
{ category = "Icon", id = "IconDock", editor = "choice", default = false, items = {false, "left", "right", "top", "bottom", "box", "ignore"}, invalidate = "layout", }, |
|
{ category = "Icon", id = "IconRows", editor = "number", default = 1, }, |
|
{ category = "Icon", id = "IconRow", editor = "number", default = 1, }, |
|
{ category = "Icon", id = "IconColumns", editor = "number", default = 1, }, |
|
{ category = "Icon", id = "IconColumn", editor = "number", default = 1, }, |
|
{ category = "Icon", id = "IconScale", name = "Icon scale", editor = "point2d", default = point(1000, 1000), help = "Used when the image is not resized (ImageFit equals 'none')", lock_ratio = true, }, |
|
{ category = "Icon", id = "IconColor", name = "Icon color", editor = "color", default = RGB(255, 255, 255), }, |
|
{ category = "Icon", id = "DisabledIconColor", name = "Disabled icon color", editor = "color", default = RGBA(255, 255, 255, 128), }, |
|
{ category = "Icon", id = "IconDesaturation", name = "Icon desaturation", editor = "number", default = 0, min = 0, max = 255, slider = true, }, |
|
{ category = "Icon", id = "IconDisabledDesaturation", name = "Disabled icon desaturation", editor = "number", default = 255, min = 0, max = 255, slider = true, }, |
|
{ category = "Icon", id = "IconFlipX", editor = "bool", default = false, invalidate = true, }, |
|
{ category = "Icon", id = "IconFlipY", editor = "bool", default = false, invalidate = true, }, |
|
}, |
|
} |
|
|
|
function XEmbedIcon:Init(parent, context) |
|
local icon = XImage:new({ |
|
Id = "idIcon", |
|
HAlign = "center", |
|
VAlign = "center", |
|
}, self, context) |
|
self:SetIcon(self.Icon) |
|
icon:SetRows(self.IconRows) |
|
icon:SetRow(self.IconRow) |
|
icon:SetColumns(self.IconColumns) |
|
icon:SetColumn(self.IconColumn) |
|
icon:SetImageScale(self.IconScale) |
|
icon:SetImageColor(self.IconColor) |
|
icon:SetDisabledImageColor(self.DisabledIconColor) |
|
icon:SetDisabledDesaturation(self.IconDisabledDesaturation) |
|
icon:SetDesaturation(self.IconDesaturation) |
|
icon:SetImageFit("scale-down") |
|
end |
|
|
|
LinkPropertyToChild(XEmbedIcon, "Icon", "idIcon", "Image") |
|
LinkPropertyToChild(XEmbedIcon, "IconDock", "idIcon", "Dock") |
|
LinkPropertyToChild(XEmbedIcon, "IconRows", "idIcon", "Rows") |
|
LinkPropertyToChild(XEmbedIcon, "IconRow", "idIcon", "Row") |
|
LinkPropertyToChild(XEmbedIcon, "IconColumns", "idIcon", "Columns") |
|
LinkPropertyToChild(XEmbedIcon, "IconColumn", "idIcon", "Column") |
|
LinkPropertyToChild(XEmbedIcon, "IconScale", "idIcon", "ImageScale") |
|
LinkPropertyToChild(XEmbedIcon, "IconColor", "idIcon", "ImageColor") |
|
LinkPropertyToChild(XEmbedIcon, "DisabledIconColor", "idIcon", "DisabledImageColor") |
|
LinkPropertyToChild(XEmbedIcon, "IconDesaturation", "idIcon", "Desaturation") |
|
LinkPropertyToChild(XEmbedIcon, "IconDisabledDesaturation", "idIcon", "DisabledDesaturation") |
|
LinkPropertyToChild(XEmbedIcon, "IconFlipX", "idIcon", "FlipX") |
|
LinkPropertyToChild(XEmbedIcon, "IconFlipY", "idIcon", "FlipY") |
|
|
|
function XEmbedIcon:SetIcon(icon) |
|
if self.idIcon:GetImage() == icon then return end |
|
self.idIcon:SetImage(icon) |
|
self.Icon = icon |
|
self.idIcon:SetDock(icon == "" and "ignore" or self.IconDock) |
|
self.idIcon:SetVisible(icon ~= "") |
|
end |
|
|
|
|
|
|
|
|
|
function FindXImagesAndReload(path, ximage_list) |
|
if not CanYield() then |
|
CreateRealTimeThread(FindXImagesAndReload, path) |
|
return |
|
end |
|
|
|
ximage_list = ximage_list or GetChildrenOfKind(terminal.desktop, "XImage") |
|
local compare_path = NormalizeGamePath(ConvertToOSPath(path) or path) |
|
local dir, name = SplitPath(compare_path) |
|
local list = table.map(ximage_list, function(v) return v:GetImage() end) |
|
local images = table.filter(ximage_list, function(idx, ximage) |
|
local image_path = ximage:GetImage() |
|
local image_os_path = NormalizeGamePath(ConvertToOSPath(image_path) or image_path) |
|
local imagedir, imagename, __ = SplitPath(image_os_path) |
|
return dir == imagedir and name == imagename |
|
end) |
|
|
|
for _, ximage in pairs(images) do |
|
ximage:SetImage("") |
|
end |
|
UIL.UnloadImage(path) |
|
UIL.Invalidate() |
|
while not UIL.IsImageUnloaded(path) do |
|
WaitMsg("OnRender") |
|
end |
|
UIL.RequestImage(path) |
|
UIL.Invalidate() |
|
for _, ximage in pairs(images) do |
|
ximage:SetImage(path) |
|
end |
|
end |
|
|
|
function EnableUIBlur(value) |
|
hr.UILBlurTextureScale = value and 500 or 0 |
|
end |
|
|
|
DefineClass.XBlurRect = { |
|
__parents = { "XWindow" }, |
|
|
|
properties = { |
|
{ category = "Blur", id = "TintColor", name = "Tint Color", editor = "color", default = RGB(180, 180, 180), }, |
|
{ category = "Blur", id = "BlurRadius", name = "Blur Radius", editor = "number", default = 150, min = 10, max = 300, slider = true, }, |
|
{ category = "Blur", id = "Mask", name = "Blur Mask", editor = "ui_image", default = "", image_preview_size = 100, }, |
|
{ category = "Blur", id = "FrameLeft", editor = "number", default = 0, invalidate = true, }, |
|
{ category = "Blur", id = "FrameTop", editor = "number", default = 0, invalidate = true, }, |
|
{ category = "Blur", id = "FrameRight", editor = "number", default = 0, invalidate = true, }, |
|
{ category = "Blur", id = "FrameBottom", editor = "number", default = 0, invalidate = true, }, |
|
{ category = "Blur", id = "Desaturation", editor = "number", default = 0, min = 0, max = 255, slider = true, invalidate = true, }, |
|
}, |
|
|
|
image_id = const.InvalidResourceID, |
|
image_obj = false, |
|
cached_image_rect = box(0, 0, 1, 1), |
|
} |
|
|
|
|
|
function XBlurRect:Init() |
|
self:SetMask(self.Mask, true) |
|
end |
|
|
|
function XBlurRect:Done() |
|
if self.image_obj ~= false then |
|
self.image_obj:ReleaseRef() |
|
self.image_obj = false |
|
end |
|
end |
|
|
|
function XBlurRect:SetMask(image, force) |
|
if self.Mask == (image or "") and not force then return end |
|
self.Mask = image or nil |
|
|
|
self:DeleteThread("LoadImage") |
|
if (self.Mask or "") == "" then |
|
return |
|
end |
|
|
|
if self.image_obj ~= false then |
|
self.image_obj:ReleaseRef() |
|
self.image_obj = false |
|
end |
|
|
|
self.image_id = ResourceManager.GetResourceID(self.Mask) |
|
if self.image_id == const.InvalidResourceID then |
|
printf("once", "Could not load image %s!", self.Mask or "") |
|
return |
|
end |
|
|
|
self.image_obj = ResourceManager.GetResource(self.image_id) |
|
if self.image_obj then |
|
self.cached_image_rect = box(0, 0, self.image_obj:GetWidth(), self.image_obj:GetHeight()) |
|
self:Invalidate() |
|
else |
|
self:CreateThread("LoadImage", function(self) |
|
self.image_obj = AsyncGetResource(self.image_id) |
|
if self.image_obj then |
|
self.cached_image_rect = box(0, 0, self.image_obj:GetWidth(), self.image_obj:GetHeight()) |
|
self:Invalidate() |
|
end |
|
end, self) |
|
end |
|
end |
|
|
|
|
|
function XBlurRect:DrawBackground() |
|
local desaturation = UIL.GetDesaturation() |
|
UIL.SetDesaturation(self.Desaturation) |
|
if hr.UILBlurTextureScale > 0 then |
|
UIL.DrawBackBufferRect(self.content_box, self.cached_image_rect, MulDivRound(self.BlurRadius, self.scale:x(), 1000), self.TintColor, self.Mask or "", |
|
self.scale, self.FrameLeft, self.FrameTop, self.FrameRight, self.FrameBottom) |
|
else |
|
local new_color = MulDivRound(point(GetRGB(self.TintColor)), 80, 100) |
|
self.Background = RGBA(0, 0, 0, Min(new_color:xyz())) |
|
XWindow.DrawBackground(self) |
|
end |
|
UIL.SetDesaturation(desaturation) |
|
end |
|
|
|
function TestXEdgeFadingImage() |
|
local parent = XWindow:new({ |
|
}, terminal.desktop) |
|
XImage:new({ Image = "UI/SplashScreen", HAlign = "center", VAlign = "center", }, parent) |
|
end |