Skip to content

Commit

Permalink
Create tooltips.lua
Browse files Browse the repository at this point in the history
  • Loading branch information
TymurGubayev authored Jan 4, 2025
1 parent 1bb3173 commit 1ed5e22
Showing 1 changed file with 281 additions and 0 deletions.
281 changes: 281 additions & 0 deletions gui/tooltips.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
-- Show tooltips on units and/or mouse

local RELOAD = false -- set to true when actively working on this script

local gui = require('gui')
local widgets = require('gui.widgets')
local ResizingPanel = require('gui.widgets.containers.resizing_panel')

--------------------------------------------------------------------------------

local follow_units = true;
local follow_mouse = true;
local function change_follow_units(new, old)
follow_units = new
end
local function change_follow_mouse(new, old)
follow_mouse = new
end

local shortenings = {
["Store item in stockpile"] = "Store item",
}

--------------------------------------------------------------------------------

local TITLE = "Tooltips"

if RELOAD then TooltipControlWindow = nil end
TooltipControlWindow = defclass(TooltipControlWindow, widgets.Window)
TooltipControlWindow.ATTRS {
frame_title=TITLE,
frame_inset=0,
resizable=false,
frame = {
w = 25,
h = 4,
-- just under the minimap:
r = 2,
t = 18,
},
}

function TooltipControlWindow:init()
self:addviews{
widgets.ToggleHotkeyLabel{
view_id = 'btn_follow_units',
frame={t=0, h=1},
label="Follow units",
key='CUSTOM_ALT_U',
on_change=change_follow_units,
},
widgets.ToggleHotkeyLabel{
view_id = 'btn_follow_mouse',
frame={t=1, h=1},
label="Follow mouse",
key='CUSTOM_ALT_M',
on_change=change_follow_mouse,
},
}
end

local function GetUnitJob(unit)
local job = unit.job
if job and job.current_job then
return dfhack.job.getName(job.current_job)
end
return nil
end

local function GetUnitNameAndJob(unit)
local sb = {}
sb[#sb+1] = dfhack.units.getReadableName(unit)
local jobName = GetUnitJob(unit)
if jobName then
sb[#sb+1] = ": "
sb[#sb+1] = jobName
end
return table.concat(sb)
end

local function GetTooltipText(x,y,z)
local txt = {}
local units = dfhack.units.getUnitsInBox(x,y,z,x,y,z) or {} -- todo: maybe (optionally) use filter parameter here?

for _,unit in pairs(units) do
txt[#txt+1] = GetUnitNameAndJob(unit)
txt[#txt+1] = NEWLINE
end

return txt
end

--------------------------------------------------------------------------------
-- MouseTooltip is an almost copy&paste of the DimensionsTooltip
--
if RELOAD then MouseTooltip = nil end
MouseTooltip = defclass(MouseTooltip, ResizingPanel)

MouseTooltip.ATTRS{
frame_style=gui.FRAME_THIN,
frame_background=gui.CLEAR_PEN,
no_force_pause_badge=true,
auto_width=true,
display_offset={x=3, y=3},
}

function MouseTooltip:init()
ensure_key(self, 'frame').w = 17
self.frame.h = 4

self.label = widgets.Label{
frame={t=0},
auto_width=true,
}

self:addviews{
widgets.Panel{
-- set minimum size for tooltip frame so the DFHack frame badge fits
frame={t=0, l=0, w=7, h=2},
},
self.label,
}
end

function MouseTooltip:render(dc)
if not follow_mouse then return end

local x, y = dfhack.screen.getMousePos()
if not x then return end

local pos = dfhack.gui.getMousePos()
local text = GetTooltipText(pos2xyz(pos))
if #text == 0 then return end
self.label:setText(text)

local sw, sh = dfhack.screen.getWindowSize()
local frame_width = math.max(9, self.label:getTextWidth() + 2)
self.frame.l = math.min(x + self.display_offset.x, sw - frame_width)
self.frame.t = math.min(y + self.display_offset.y, sh - self.frame.h)
self:updateLayout()
MouseTooltip.super.render(self, dc)
end

--------------------------------------------------------------------------------

if RELOAD then TooltipsVizualizer = nil end
TooltipsVizualizer = defclass(TooltipsVizualizer, gui.ZScreen)
TooltipsVizualizer.ATTRS{
focus_path='TooltipsVizualizer',
pass_movement_keys=true,
}

function TooltipsVizualizer:init()
local controls = TooltipControlWindow{view_id = 'controls'}
local tooltip = MouseTooltip{view_id = 'tooltip'}
self:addviews{controls, tooltip}
end

-- map coordinates -> interface layer coordinates
function GetScreenCoordinates(map_coord)
if not map_coord then return end

-- -> map viewport offset
local vp = df.global.world.viewport
local vp_Coord = vp.window_x -- is actually coord
local map_offset_by_vp = {
x = map_coord.x - vp_Coord.x,
y = map_coord.y - vp_Coord.y,
z = map_coord.z - vp_Coord.z,
}
-- -> pixel offset
local gps = df.global.gps
local map_tile_pixels = gps.viewport_zoom_factor // 4;
local screen_coord_px = {
x = map_tile_pixels * map_offset_by_vp.x,
y = map_tile_pixels * map_offset_by_vp.y,
}
-- -> interface layer coordinates
local screen_coord_text = {
x = math.ceil( screen_coord_px.x / gps.tile_pixel_x ),
y = math.ceil( screen_coord_px.y / gps.tile_pixel_y ),
}

return screen_coord_text
end

function TooltipsVizualizer:onRenderFrame(dc, rect)
TooltipsVizualizer.super.onRenderFrame(self, dc, rect)

if not follow_units then return end

if not dfhack.screen.inGraphicsMode() and not gui.blink_visible(500) then
return
end

local vp = df.global.world.viewport
local topleft = vp.window_x
local width = vp.max_x
local height = vp.max_y
local bottomright = {x = topleft.x + width, y = topleft.y + height, z = topleft.z}

local units = dfhack.units.getUnitsInBox(topleft.x,topleft.y,topleft.z,bottomright.x,bottomright.y,bottomright.z) or {}
if #units == 0 then return end

local oneTileOffset = GetScreenCoordinates({x = topleft.x + 1, y = topleft.y + 1, z = topleft.z + 0})
local pen = COLOR_WHITE

local used_tiles = {}
for i = #units, 1, -1 do
local unit = units[i]
local txt = GetUnitJob(unit)
if not txt then goto continue end

txt = shortenings[txt] or txt

local pos = xyz2pos(dfhack.units.getPosition(unit))
if not pos then goto continue end

local scrPos = GetScreenCoordinates(pos)
local y = scrPos.y - 1 -- subtract 1 to move the text over the heads
local x = scrPos.x + oneTileOffset.x - 1 -- subtract 1 to move the text inside the map tile

-- to resolve overlaps, we'll mark every coordinate we write anything in,
-- and then check if the new tooltip will overwrite any used coordinate.
-- if it will, try the next row, to a maximum offset of 4.
local row
local dy = 0
-- todo: search for the "best" offset instead, f.e. max `usedAt` value, with `-1` the best
local usedAt = -1
for yOffset = 0, 4 do
dy = yOffset

row = used_tiles[y + dy]
if not row then
row = {}
used_tiles[y + dy] = row
end

usedAt = -1
for j = 0, #txt - 1 do
if row[x + j] then
usedAt = j
break
end
end

if usedAt == -1 then break end
end -- for dy

-- in case there isn't enough space, cut the text off
if usedAt > 0 then
txt = txt:sub(0, usedAt - 1) .. '_'
end

dc:seek(x, y + dy):pen(pen):string(txt)

-- mark coordinates as used
for j = 0, #txt - 1 do
row[x + j] = true
end

::continue::
end
end

function TooltipsVizualizer:onDismiss()
view = nil
end

----------------------------------------------------------------

if not dfhack.isMapLoaded() then
qerror('gui/tooltips requires a map to be loaded')
end

if RELOAD and view then
view:dismiss()
-- view is nil now
end

view = view and view:raise() or TooltipsVizualizer{}:show()

0 comments on commit 1ed5e22

Please sign in to comment.