Skip to content

Commit

Permalink
Added always_on_top option (on by default) to render the final textur…
Browse files Browse the repository at this point in the history
…es in an IMGUI window to ensure Balloon always appears in front of all other custom UI elements
  • Loading branch information
onimitch committed Feb 28, 2024
1 parent 76e7b6f commit 32a5b5d
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 74 deletions.
13 changes: 10 additions & 3 deletions Balloon.lua
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ local function print_help(isError)
{ '/balloon speed <chars per second>', 'Speed that text is displayed, in characters per second.' },
{ '/balloon portrait', 'Toggle the display of character portraits, if the theme has settings for them.' },
{ '/balloon move_closes', 'Toggle balloon auto-close on player movement.' },
{ '/balloon always_on_top', 'Toggle always on top (IMGUI mode).' },
{ '/balloon test <name> <lang> <mode>', 'Display a test bubble. Lang: - (auto), en or ja. Mode: 1 (dialogue), 2 (system). "/balloon test" to see the list of available tests.' },
}

Expand Down Expand Up @@ -532,7 +533,7 @@ ashita.events.register('command', 'balloon_command_cb', function(e)

if #args > 2 then
local old_val = balloon.settings[setting_key]
balloon.settings[setting_key] = tonumber(args[3])
balloon.settings[setting_key] = tonumber(args[3]) or 0

-- Some additional logic we need to run depending on the setting change
if setting_key == 'scale' then
Expand All @@ -552,14 +553,15 @@ ashita.events.register('command', 'balloon_command_cb', function(e)
-- Handle toggle options
-- Handle: /balloon portrait
-- Handle: /balloon move_closes
if (#args == 2 and args[2]:any('portrait', 'portraits', 'move_closes', 'move_close')) then
if (#args == 2 and args[2]:any('portrait', 'portraits', 'move_closes', 'move_close', 'always_on_top')) then
local setting_key_alias = {
portrait = 'portraits',
move_closes = 'move_close',
}
local setting_names = {
portraits = 'Display portraits',
move_close = 'Close balloons on player movement',
always_on_top = 'Always on top (IMGUI mode)',
}
local setting_key = setting_key_alias[args[2]] or args[2]
local setting_name = setting_names[setting_key] or args[2]
Expand Down Expand Up @@ -730,7 +732,12 @@ ashita.events.register('d3d_present', 'balloon_d3d_present', function()
balloon.handle_player_movement(player_ent)

if not ui:hidden() then
ui:render(delta_time)

if balloon.settings.always_on_top then
ui:render_imgui(delta_time)
else
ui:render(delta_time)
end
end
end)

Expand Down
2 changes: 2 additions & 0 deletions defaults.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ defaults.text_speed = 100
defaults.theme = 'default'
defaults.scale = 1
defaults.portraits = true
defaults.always_on_top = true

defaults.additional_chat_modes = {
144
}
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ You can use `/balloon` or `/bl`

`/balloon move_closes` - Toggle balloon auto-close on player movement.

`/balloon always_on_top` - Toggle always on top (IMGUI mode). This mode renders the final elements using IMGUI to ensure Balloon always appears in front of any other custom UI. If for some reason you have issues with this mode, you can use this command to disable it.

`/balloon test <name> <lang> <mode>` - Display a test bubble. Lang: "-" (auto), "en" or "ja". Mode: 1 (dialogue), 2 (system).

`/balloon test` - List all available tests.
Expand Down
178 changes: 107 additions & 71 deletions ui.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ local C = ffi.C
local d3d8dev = d3d.get_device()

local gdi = require('gdifonts.include')
local imgui = require('imgui')

local PI2 = math.pi*2

Expand Down Expand Up @@ -37,9 +38,7 @@ ui._show_portraits = true
ui._theme_options = nil

ui._sprite = nil
ui._rect = ffi.new('RECT', { 0, 0, 100, 100, })
ui._vec_position = ffi.new('D3DXVECTOR2', { 0, 0, })
ui._vec_scale = ffi.new('D3DXVECTOR2', { 1.0, 1.0, })
ui._bounds = { 0, 0, 0, 0 }


local function setup_image(image, path)
Expand Down Expand Up @@ -246,6 +245,8 @@ function ui:position(x, y, topleft_anchor)
self.message_background:pos(x, y)
self.message_background:size(self._theme_options.message.width * self._scale, self._theme_options.message.height * self._scale)

local elements = {}

if self._theme_options.portrait then
local portrait_offset_x = self._theme_options.portrait.offset_x * self._scale
local portrait_offset_y = self._theme_options.portrait.offset_y * self._scale
Expand All @@ -255,35 +256,56 @@ function ui:position(x, y, topleft_anchor)
self.portrait:size(self._theme_options.portrait.width * self._scale, self._theme_options.portrait.height * self._scale)
self.portrait_frame:pos(x + portrait_offset_x, y + portrait_offset_y)
self.portrait_frame:size(self._theme_options.portrait.width * self._scale, self._theme_options.portrait.height * self._scale)

table.insert(elements, self.portrait_background)
table.insert(elements, self.portrait)
table.insert(elements, self.portrait_frame)
end

self.name_background:pos(x + name_bg_offset_x, y + name_bg_offset_y)
self.name_background:size(self._theme_options.name.width * self._scale, self._theme_options.name.height * self._scale)
table.insert(elements, self.name_background)

if self._theme_options.prompt then
local prompt_offset_x = self._theme_options.prompt.offset_x * self._scale
local prompt_offset_y = self._theme_options.prompt.offset_y * self._scale
self.prompt:pos(x + prompt_offset_x, y + prompt_offset_y)
self.prompt:size(self._theme_options.prompt.width * self._scale, self._theme_options.prompt.height * self._scale)

table.insert(elements, self.prompt)
end

self.message_text:pos(x + message_text_offset_x, y + message_text_offset_y)
self.message_text:size(self._theme_options.message.font_size * self._scale)

local message_text_width = (self._theme_options.message.width - self._theme_options.message.margin_right) * self._scale - message_text_offset_x
local message_text_height = self._theme_options.message.height * self._scale
self.message_text:width(message_text_width)
self.message_text:height(message_text_height)
table.insert(elements, self.message_text)

self.name_text:pos(x + name_text_offset_x, y + name_text_offset_y)
self.name_text:size(self._theme_options.name.font_size * self._scale)
table.insert(elements, self.name_text)

if self._theme_options.timer then
local timer_text_offset_x = self._theme_options.timer.offset_x * self._scale
local timer_text_offset_y = self._theme_options.timer.offset_y * self._scale
self.timer_text:pos(x + timer_text_offset_x, y + timer_text_offset_y)
self.timer_text:size(self._theme_options.timer.font_size * self._scale)
table.insert(elements, self.timer_text)
end

-- Calculate window bounds
local bounds = { self.message_background:pos_x(), self.message_background:pos_y(),
self.message_background:width(), self.message_background:height() }

for _, element in ipairs(elements) do
bounds[1] = math.min(bounds[1], element:pos_x())
bounds[2] = math.min(bounds[2], element:pos_y())
bounds[3] = math.max(bounds[3], element:pos_x() + (element:width() or 0))
bounds[4] = math.max(bounds[4], element:pos_y() + (element:height() or 0))
end
self._bounds = bounds
end

function ui:hide()
Expand Down Expand Up @@ -340,7 +362,6 @@ end

function ui:set_type(type)
local types = {
--[190] = self._system_settings, -- system text (always a duplicate of 151?)
[150] = self._dialogue_settings, -- npc text
[151] = self._system_settings, -- system text
[142] = self._dialogue_settings, -- battle text
Expand Down Expand Up @@ -414,40 +435,6 @@ function ui:update_message_bg(path)
end
end

local function Tokenize(str)
local result = {}
for word in str:gmatch("%S+") do
result[#result+1] = word
end
return result
end

function ui:wrap_text(str)
local line_length = self._theme_options.message.max_length+1
if self._has_portrait and self._theme_options.portrait.max_length then
line_length = self._theme_options.portrait.max_length+1
end
local length_left = line_length
local result = {}
local line = {}

for _, word in ipairs(Tokenize(str)) do
if #word+1 > length_left then
table.insert(result, table.concat(line, ' '))
line = {word}
length_left = line_length - #word
else
table.insert(line, word)
length_left = length_left - (#word + 1)
end
end

table.insert(result, table.concat(line, ' '):trimex())
local new_str = table.concat(result, '\n '):trimex()

return new_str
end

function ui:set_message(message)
message = message or ''
self._current_text = message
Expand Down Expand Up @@ -505,47 +492,26 @@ function ui:hidden()
return self._hidden
end

local function render_image(sprite, image)
if not image:visible() then
return
end

local texture = image:texture()
local vec_position = ui._vec_position
local vec_scale = ui._vec_scale
local rect = ui._rect

rect.right = texture.width
rect.bottom = texture.height
vec_position.x = image:pos_x()
vec_position.y = image:pos_y()

-- Calc correct scale to render at
vec_scale.x = image:width() / texture.width
vec_scale.y = image:height() / texture.height

local red, green, blue = image:color()
local color = d3d.D3DCOLOR_ARGB(image:alpha(), red, green, blue)

sprite:Draw(image:texture().ptr, rect, vec_scale, nil, 0.0, vec_position, color)
function ui:tick(delta_time)
self:animate_prompt(delta_time)
self:animate_text_display(self._text_speed * delta_time)
end

function ui:render(delta_time)
if (self._sprite == nil) then return end
self:tick(delta_time)

self:animate_prompt(delta_time)
self:animate_text_display(self._text_speed * delta_time)
if self._sprite == nil then return end

local sprite = self._sprite

sprite:Begin()

render_image(sprite, self.message_background)
render_image(sprite, self.portrait_background)
render_image(sprite, self.portrait)
render_image(sprite, self.portrait_frame)
render_image(sprite, self.name_background)
render_image(sprite, self.prompt)
self.message_background:render(sprite)
self.portrait_background:render(sprite)
self.portrait:render(sprite)
self.portrait_frame:render(sprite)
self.name_background:render(sprite)
self.prompt:render(sprite)

self.message_text:render(sprite)
self.name_text:render(sprite)
Expand All @@ -554,4 +520,74 @@ function ui:render(delta_time)
sprite:End()
end

function render_image_imgui(image)
if not image:visible() then
return
end

local texture = image:texture()
local red, green, blue = image:color()
local alpha = image:alpha()

imgui.SetCursorScreenPos({ image:pos_x(), image:pos_y() })
imgui.Image(tonumber(ffi.cast('uint32_t', texture.ptr)),
{ image:width(), image:height() },
{ 0, 0 },
{ 1, 1 },
{ red / 255, green / 255, blue / 255, alpha / 255 })
end

function render_fontobject_imgui(text_obj)
local fontobject = text_obj:font_object()
if fontobject == nil then
return
end

if fontobject.settings.visible ~= true or fontobject.settings.opacity == 0 then
return
end

local texture, rect = fontobject:get_texture()
if (texture ~= nil) then
local x = fontobject.settings.position_x
if (fontobject.settings.font_alignment == 1) then
x = fontobject.settings.position_x - (rect.right / 2)
elseif (fontobject.settings.font_alignment == 2) then
x = fontobject.settings.position_x - rect.right
end
local y = fontobject.settings.position_y

imgui.SetCursorScreenPos({ x, y })
imgui.Image(tonumber(ffi.cast('uint32_t', texture)),
{ rect.right, rect.bottom },
{ 0, 0 },
{ 1, 1 },
{ 1, 1, 1, fontobject.settings.opacity })
end
end

function ui:render_imgui(delta_time)
self:tick(delta_time)

imgui.SetNextWindowPos({ self._bounds[1], self._bounds[2] }, ImGuiCond_Always)
imgui.SetNextWindowSize({ self._bounds[3], self._bounds[4] }, ImGuiCond_Always)
imgui.SetNextWindowFocus()

local windowFlags = bit.bor(ImGuiWindowFlags_NoDecoration, ImGuiWindowFlags_NoFocusOnAppearing, ImGuiWindowFlags_NoNav, ImGuiWindowFlags_NoBackground, ImGuiWindowFlags_NoMove)

if imgui.Begin('Balloon', true, windowFlags) then
render_image_imgui(self.message_background)
render_image_imgui(self.portrait_background)
render_image_imgui(self.portrait)
render_image_imgui(self.portrait_frame)
render_image_imgui(self.name_background)
render_image_imgui(self.prompt)

render_fontobject_imgui(self.message_text)
render_fontobject_imgui(self.name_text)
render_fontobject_imgui(self.timer_text)
end
imgui.End()
end

return ui
28 changes: 28 additions & 0 deletions wlibs/images.lua
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,34 @@ function images.unregister_event(t, key, fn)
end
end

local vec_position = ffi.new('D3DXVECTOR2', { 0, 0 })
local vec_scale = ffi.new('D3DXVECTOR2', { 1.0, 1.0 })
local rect = ffi.new('RECT', { 0, 0, 100, 100 })

function images:render(sprite)
if not self:visible() then
return
end

local texture = self:texture()

rect.top = 0
rect.left = 0
rect.right = texture.width
rect.bottom = texture.height
vec_position.x = self:pos_x()
vec_position.y = self:pos_y()

-- Calc correct scale to render at
vec_scale.x = self:width() / texture.width
vec_scale.y = self:height() / texture.height

local red, green, blue = self:color()
local color = d3d.D3DCOLOR_ARGB(self:alpha(), red, green, blue)

sprite:Draw(texture.ptr, rect, vec_scale, nil, 0.0, vec_position, color)
end

return images

--[[
Expand Down
4 changes: 4 additions & 0 deletions wlibs/texts.lua
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,10 @@ function texts.render(t, sprite)
return meta[t].font_object:render(sprite)
end

function texts.font_object(t)
return meta[t].font_object
end

local function count_utf8_chars(str)
if utf8 and utf8.len then
return utf8.len(str)
Expand Down

0 comments on commit 32a5b5d

Please sign in to comment.