dfhack/library/lua/gui.lua

1046 lines
29 KiB
Lua

-- Viewscreen implementation utility collection.
local _ENV = mkmodule('gui')
2023-08-27 21:58:30 -06:00
local textures = require('gui.textures')
local utils = require('utils')
local dscreen = dfhack.screen
local getval = utils.getval
local to_pen = dfhack.pen.parse
CLEAR_PEN = to_pen{tile=dfhack.internal.getAddress('init') and df.global.init.texpos_border_interior, ch=32, fg=0, bg=0, write_to_lower=true}
TRANSPARENT_PEN = to_pen{tile=0, ch=0}
KEEP_LOWER_PEN = to_pen{ch=32, fg=0, bg=0, keep_lower=true}
local MOUSE_KEYS = {
_MOUSE_L = true,
_MOUSE_R = true,
_MOUSE_M = true,
2012-12-02 03:43:23 -07:00
_MOUSE_L_DOWN = true,
_MOUSE_R_DOWN = true,
_MOUSE_M_DOWN = true,
}
local FAKE_INPUT_KEYS = copyall(MOUSE_KEYS)
FAKE_INPUT_KEYS._STRING = true
function simulateInput(screen,...)
local keys = {}
local function push_key(arg)
local kv = arg
if type(arg) == 'string' then
kv = df.interface_key[arg]
if kv == nil and not FAKE_INPUT_KEYS[arg] then
error('Invalid keycode: '..arg)
end
end
if type(kv) == 'number' then
keys[#keys+1] = kv
end
end
for i = 1,select('#',...) do
local arg = select(i,...)
if arg ~= nil then
local t = type(arg)
if type(arg) == 'table' then
for k,v in pairs(arg) do
if v == true then
push_key(k)
else
push_key(v)
end
end
else
push_key(arg)
end
end
end
dscreen._doSimulateInput(screen, keys)
end
function mkdims_xy(x1,y1,x2,y2)
return { x1=x1, y1=y1, x2=x2, y2=y2, width=x2-x1+1, height=y2-y1+1 }
end
function mkdims_wh(x1,y1,w,h)
return { x1=x1, y1=y1, x2=x1+w-1, y2=y1+h-1, width=w, height=h }
end
function is_in_rect(rect,x,y)
return x and y and x >= rect.x1 and x <= rect.x2 and y >= rect.y1 and y <= rect.y2
end
local function align_coord(gap,align,lv,rv)
if gap <= 0 then
return 0
end
if not align then
if rv and not lv then
align = 1.0
elseif lv and not rv then
align = 0.0
else
align = 0.5
end
end
return math.floor(gap*align)
end
function compute_frame_rect(wavail,havail,spec,xgap,ygap)
if not spec then
return mkdims_wh(0,0,wavail,havail)
end
local sw = wavail - (spec.l or 0) - (spec.r or 0)
local sh = havail - (spec.t or 0) - (spec.b or 0)
local rqw = math.min(sw, (spec.w or sw)+xgap)
local rqh = math.min(sh, (spec.h or sh)+ygap)
local ax = align_coord(sw - rqw, spec.xalign, spec.l, spec.r)
local ay = align_coord(sh - rqh, spec.yalign, spec.t, spec.b)
local rect = mkdims_wh((spec.l or 0) + ax, (spec.t or 0) + ay, rqw, rqh)
rect.wgap = sw - rqw
rect.hgap = sh - rqh
return rect
end
add scroll icons to Label widget (#2101) * WIP: add scroll icons to Label widget It's an opt-out. The icons are rendered in the right-most column of the 1st and last row. They are only rendered when text can actually be scrolled in the corresponding direction. WIP: Currently, the icons might overlay text characters, there is no mechanism preventing it * gui.lua: expose the `parse_inset()` function * refactor Label's scroll icon code * since `render_scroll_icons` only works with a label, it's now a class function * `update_scroll_inset` ensures `frame_inset.r` or `.l` is at least 1, according to `show_scroll_icons` * `show_scroll_icons` has 4 possible values: `false` for no icons, `left` for icons on the first column on the left (also ensuring `frame_inset.l >= 1`), `right` - last column on the right, `DEFAULT_NIL` - same as `right` if text height greater than `frame_body.height`, else same as `false`. * make `render_scroll_icons` always draw icons The check now happens in `onRenderFrame` * draw frame's background calling `Label.super.onRenderFrame(self, dc, rect)` makes frame's background invisible for some reason * remove trailing spaces * fix scroll icons placed far above/below text With `Label.frame_inset = 1` the text could be vertically centered with plenty of space below and above, but not all rendered. Before this change, the scroll icons would be at the very top and bottom of the frame instead of near the first and last rendered text line. * always `update_scroll_inset` to react to resized window * draw scroll icons next to text * update `Lua API.rst` with new `Label` parameters * move comment separator up This way every scroll related parameter is in one group * list default values for new parameters in docs * add missing description of `Label.scroll_keys`
2022-04-29 07:55:08 -06:00
function parse_inset(inset)
local l,r,t,b
if type(inset) == 'table' then
l,r = inset.l or inset.x or 0, inset.r or inset.x or 0
t,b = inset.t or inset.y or 0, inset.b or inset.y or 0
else
l = inset or 0
t,r,b = l,l,l
end
return l,t,r,b
end
function inset_frame(rect, inset, gap)
if not rect then return mkdims_wh(0, 0, 0, 0) end
gap = gap or 0
local l,t,r,b = parse_inset(inset)
return mkdims_xy(rect.x1+l+gap, rect.y1+t+gap, rect.x2-r-gap, rect.y2-b-gap)
end
function compute_frame_body(wavail, havail, spec, inset, gap, inner_frame)
gap = gap or 0
local l,t,r,b = parse_inset(inset)
local xgap,ygap = 0,0
if inner_frame then
xgap,ygap = gap*2+l+r, gap*2+t+b
end
local rect = compute_frame_rect(wavail, havail, spec, xgap, ygap)
local body = mkdims_xy(rect.x1+l+gap, rect.y1+t+gap, rect.x2-r-gap, rect.y2-b-gap)
return rect, body
end
function blink_visible(delay)
return math.floor(dfhack.getTickCount()/delay) % 2 == 0
end
function getKeyDisplay(code)
if type(code) == 'string' then
code = df.interface_key[code]
end
return dscreen.getKeyDisplay(code)
end
-----------------------------------
-- Clipped view rectangle object --
-----------------------------------
ViewRect = defclass(ViewRect, nil)
function ViewRect:init(args)
if args.view_rect then
self:assign(args.view_rect)
else
local rect = args.rect or mkdims_wh(0,0,dscreen.getWindowSize())
local crect = args.clip_rect or rect
self:assign{
x1 = rect.x1, clip_x1 = crect.x1,
y1 = rect.y1, clip_y1 = crect.y1,
x2 = rect.x2, clip_x2 = crect.x2,
y2 = rect.y2, clip_y2 = crect.y2,
width = rect.x2-rect.x1+1,
height = rect.y2-rect.y1+1,
}
end
if args.clip_view then
local cr = args.clip_view
self:assign{
clip_x1 = math.max(self.clip_x1, cr.clip_x1),
clip_y1 = math.max(self.clip_y1, cr.clip_y1),
clip_x2 = math.min(self.clip_x2, cr.clip_x2),
clip_y2 = math.min(self.clip_y2, cr.clip_y2),
}
end
end
function ViewRect:isDefunct()
return (self.clip_x1 > self.clip_x2 or self.clip_y1 > self.clip_y2)
end
function ViewRect:inClipGlobalXY(x,y)
return x >= self.clip_x1 and x <= self.clip_x2
and y >= self.clip_y1 and y <= self.clip_y2
end
function ViewRect:inClipLocalXY(x,y)
return (x+self.x1) >= self.clip_x1 and (x+self.x1) <= self.clip_x2
and (y+self.y1) >= self.clip_y1 and (y+self.y1) <= self.clip_y2
end
function ViewRect:localXY(x,y)
return x-self.x1, y-self.y1
end
function ViewRect:globalXY(x,y)
return x+self.x1, y+self.y1
end
function ViewRect:viewport(x,y,w,h)
if type(x) == 'table' then
x,y,w,h = x.x1, x.y1, x.width, x.height
end
local x1,y1 = self.x1+x, self.y1+y
local x2,y2 = x1+w-1, y1+h-1
local vp = {
-- Logical viewport
x1 = x1, y1 = y1, x2 = x2, y2 = y2,
width = w, height = h,
-- Actual clipping rect
clip_x1 = math.max(self.clip_x1, x1),
clip_y1 = math.max(self.clip_y1, y1),
clip_x2 = math.min(self.clip_x2, x2),
clip_y2 = math.min(self.clip_y2, y2),
}
return mkinstance(ViewRect, vp)
end
----------------------------
-- Clipped painter object --
----------------------------
Painter = defclass(Painter, ViewRect)
function Painter:init(args)
self.x = self.x1
self.y = self.y1
self.cur_pen = to_pen(args.pen or COLOR_GREY)
self.cur_key_pen = to_pen(args.key_pen or COLOR_LIGHTGREEN)
self.to_map = false
end
function Painter.new(rect, pen)
return Painter{ rect = rect, pen = pen }
end
function Painter.new_view(view_rect, pen)
return Painter{ view_rect = view_rect, pen = pen }
end
function Painter.new_xy(x1,y1,x2,y2,pen)
return Painter{ rect = mkdims_xy(x1,y1,x2,y2), pen = pen }
end
function Painter.new_wh(x,y,w,h,pen)
return Painter{ rect = mkdims_wh(x,y,w,h), pen = pen }
end
function Painter:isValidPos()
return self:inClipGlobalXY(self.x, self.y)
end
function Painter:viewport(x,y,w,h)
local vp = ViewRect.viewport(self,x,y,w,h)
vp.cur_pen = self.cur_pen
vp.cur_key_pen = self.cur_key_pen
return mkinstance(Painter, vp):seek(0,0)
end
function Painter:cursor()
return self.x - self.x1, self.y - self.y1
end
function Painter:cursorX()
return self.x - self.x1
end
function Painter:cursorY()
return self.y - self.y1
end
function Painter:seek(x,y)
if x then self.x = self.x1 + x end
if y then self.y = self.y1 + y end
return self
end
function Painter:advance(dx,dy)
if dx then self.x = self.x + dx end
if dy then self.y = self.y + dy end
return self
end
function Painter:newline(dx)
self.y = self.y + 1
self.x = self.x1 + (dx or 0)
return self
end
function Painter:pen(pen,...)
self.cur_pen = to_pen(self.cur_pen, pen, ...)
return self
end
function Painter:color(fg,bold,bg)
self.cur_pen = to_pen(self.cur_pen, fg, bg, bold)
return self
end
function Painter:key_pen(pen,...)
self.cur_key_pen = to_pen(self.cur_key_pen, pen, ...)
return self
end
function Painter:map(to_map)
self.to_map = to_map
return self
end
function Painter:clear()
dscreen.fillRect(CLEAR_PEN, self.clip_x1, self.clip_y1, self.clip_x2, self.clip_y2)
return self
end
function Painter:fill(x1,y1,x2,y2,pen,bg,bold)
if type(x1) == 'table' then
x1, y1, x2, y2, pen, bg, bold = x1.x1, x1.y1, x1.x2, x1.y2, y1, x2, y2
end
x1 = math.max(x1+self.x1,self.clip_x1)
y1 = math.max(y1+self.y1,self.clip_y1)
x2 = math.min(x2+self.x1,self.clip_x2)
y2 = math.min(y2+self.y1,self.clip_y2)
dscreen.fillRect(to_pen(self.cur_pen,pen,bg,bold),x1,y1,x2,y2,self.to_map)
return self
end
function Painter:char(char,pen,...)
if self:isValidPos() then
dscreen.paintTile(to_pen(self.cur_pen, pen, ...), self.x, self.y, char, nil, self.to_map)
end
return self:advance(1, nil)
end
function Painter:tile(char,tile,pen,...)
if self:isValidPos() then
dscreen.paintTile(to_pen(self.cur_pen, pen, ...), self.x, self.y, char, tile, self.to_map)
end
return self:advance(1, nil)
end
function Painter:string(text,pen,...)
if self.y >= self.clip_y1 and self.y <= self.clip_y2 then
local dx = 0
if self.x < self.clip_x1 then
dx = self.clip_x1 - self.x
end
local len = #text
if self.x + len - 1 > self.clip_x2 then
len = self.clip_x2 - self.x + 1
end
if len > dx then
dscreen.paintString(
to_pen(self.cur_pen, pen, ...),
self.x+dx, self.y,
string.sub(text,dx+1,len),
self.to_map
)
end
end
return self:advance(#text, nil)
end
function Painter:key(keycode,pen,...)
return self:string(
getKeyDisplay(keycode),
to_pen(self.cur_key_pen, pen, ...)
)
end
function Painter:key_string(keycode, text, ...)
return self:key(keycode):string(': '):string(text, ...)
2017-03-01 13:56:50 -07:00
end
--------------------------
-- Abstract view object --
--------------------------
View = defclass(View)
View.ATTRS {
active = true,
visible = true,
view_id = DEFAULT_NIL,
on_focus = DEFAULT_NIL,
on_unfocus = DEFAULT_NIL,
}
function View:init(args)
self.subviews = {}
self.focus_group = {self}
self.focus = false
end
local function inherit_focus_group(view, focus_group)
for _,child in ipairs(view.subviews) do
inherit_focus_group(child, focus_group)
end
view.focus_group = focus_group
end
function View:addviews(list)
if not list then return end
local sv = self.subviews
for _,obj in ipairs(list) do
-- absorb the focus groups of new children
for _,focus_obj in ipairs(obj.focus_group) do
table.insert(self.focus_group, focus_obj)
end
-- if the child's focus group has a focus owner, absorb it if we don't
-- already have one. otherwise keep the focus owner we have and clear
-- the focus of the child.
if obj.focus_group.cur then
if not self.focus_group.cur then
self.focus_group.cur = obj.focus_group.cur
else
obj.focus_group.cur:setFocus(false)
end
end
-- overwrite the child's focus_group hierarchy with ours
inherit_focus_group(obj, self.focus_group)
-- if we don't have a focus owner, give it to the new child if they want
if not self.focus_group.cur and obj:getPreferredFocusState() then
obj:setFocus(true)
end
-- set ourselves as the parent view of the new child
obj.parent_view = self
-- add the subview to our child list
table.insert(sv, obj)
local id = obj.view_id
if id and type(id) ~= 'number' and sv[id] == nil then
sv[id] = obj
end
end
for _,dir in ipairs(list) do
2012-10-16 08:33:00 -06:00
for id,obj in pairs(dir.subviews) do
if id and type(id) ~= 'number' and sv[id] == nil then
sv[id] = obj
end
end
end
end
-- should be overridden by widgets that care about capturing keyboard focus
-- (e.g. widgets.EditField)
function View:getPreferredFocusState()
return false
end
function View:setFocus(focus)
if focus then
if self.focus then return end -- nothing to do if we already have focus
if self.focus_group.cur then
-- steal focus from current owner
self.focus_group.cur:setFocus(false)
end
self.focus_group.cur = self
self.focus = true
if self.on_focus then
self.on_focus()
end
elseif self.focus then
self.focus = false
self.focus_group.cur = nil
if self.on_unfocus then
self.on_unfocus()
end
end
end
function View:getWindowSize()
local rect = self.frame_body
return rect.width, rect.height
end
function View:getMousePos(view_rect)
local rect = view_rect or self.frame_body
local x,y = dscreen.getMousePos()
if rect and x and rect:inClipGlobalXY(x,y) then
return rect:localXY(x,y)
end
end
function View:getMouseFramePos()
return self:getMousePos(ViewRect{rect=self.frame_rect})
end
function View:computeFrame(parent_rect)
return mkdims_wh(0,0,parent_rect.width,parent_rect.height)
end
function View:updateSubviewLayout(frame_body)
for _,child in ipairs(self.subviews) do
child:updateLayout(frame_body)
end
end
function View:updateLayout(parent_rect)
if not parent_rect then
parent_rect = self.frame_parent_rect
else
self.frame_parent_rect = parent_rect
end
self:invoke_before('preUpdateLayout', parent_rect)
local frame_rect,body_rect = self:computeFrame(parent_rect)
self.frame_rect = frame_rect
self.frame_body = parent_rect:viewport(body_rect or frame_rect)
self:invoke_after('postComputeFrame', self.frame_body)
self:updateSubviewLayout(self.frame_body)
self:invoke_after('postUpdateLayout', self.frame_body)
end
function View:renderSubviews(dc)
for _,child in ipairs(self.subviews) do
if getval(child.visible) then
child:render(dc)
end
end
end
function View:render(dc)
self:onRenderFrame(dc, self.frame_rect)
local sub_dc = Painter{
view_rect = self.frame_body,
clip_view = dc
}
self:onRenderBody(sub_dc)
self:renderSubviews(sub_dc)
end
function View:onRenderFrame(dc,rect)
end
function View:onRenderBody(dc)
end
-- Returns whether we should invoke the focus owner's onInput() function from
-- the given view's inputToSubviews() function. That is, returns true if:
-- - the view is not itself the focus owner since that would be an infinite loop
-- - the view is not a descendent of the focus owner (same as above)
-- - the focus owner and all of its ancestors are visible and active, since if
-- the focus owner is not (directly or transitively) visible or active, then
-- it shouldn't be getting input.
local function should_send_input_to_focus_owner(view, focus_owner)
local iter = view
while iter do
if iter == focus_owner then
return false
end
iter = iter.parent_view
end
iter = focus_owner
while iter do
if not getval(iter.visible) or not getval(iter.active) then
return false
end
iter = iter.parent_view
end
return true
end
function View:inputToSubviews(keys)
local children = self.subviews
-- give focus owner first dibs on the input
local focus_owner = self.focus_group.cur
if focus_owner and should_send_input_to_focus_owner(self, focus_owner) and
focus_owner:onInput(keys) then
return true
end
for i=#children,1,-1 do
local child = children[i]
if getval(child.visible) and getval(child.active)
and child ~= focus_owner and child:onInput(keys) then
return true
end
end
return false
end
function View:onInput(keys)
return self:inputToSubviews(keys)
end
------------------------
-- Base screen object --
------------------------
Screen = defclass(Screen, View)
Screen.text_input_mode = false
Screen.request_full_screen_refresh = false
function Screen:postinit()
self:onResize(dscreen.getWindowSize())
end
Screen.isDismissed = dscreen.isDismissed
function Screen:isShown()
return self._native ~= nil
end
function Screen:isActive()
return self:isShown() and not self:isDismissed()
end
function Screen:invalidate()
dscreen.invalidate()
end
function Screen:renderParent()
if self._native and self._native.parent then
self._native.parent:render()
else
dscreen.clear()
end
if Screen.request_full_screen_refresh then
df.global.gps.force_full_display_count = 1
Screen.request_full_screen_refresh = false
end
end
function Screen:sendInputToParent(...)
if self._native and self._native.parent then
simulateInput(self._native.parent, ...)
end
end
function Screen:show(parent)
if self._native then
error("This screen is already on display")
end
self:onAboutToShow(parent or dfhack.gui.getCurViewscreen(true))
-- if we're autodetecting the parent, refresh the parent handle in case the
-- original parent has been dismissed by onAboutToShow()'s actions
parent = parent or dfhack.gui.getCurViewscreen(true)
if not dscreen.show(self, parent.child) then
error('Could not show screen')
end
return self
end
function Screen:onAboutToShow(parent)
end
function Screen:onShow()
self:onResize(dscreen.getWindowSize())
end
function Screen:dismiss()
if self._native then
dscreen.dismiss(self)
end
-- don't leave artifacts behind on the parent screen when we disappear
Screen.request_full_screen_refresh = true
end
function Screen:onDismiss()
end
function Screen:onDestroy()
end
function Screen:onResize(w,h)
self:updateLayout(ViewRect{ rect = mkdims_wh(0,0,w,h) })
end
function Screen:onRender()
self:render(Painter.new())
end
-----------------------------
-- Z-order swapping screen --
-----------------------------
2023-01-27 15:57:45 -07:00
DEFAULT_INITIAL_PAUSE = true
local zscreen_inhibit_mouse_l = false
-- ensure underlying DF screens don't also react to handled clicks
function markMouseClicksHandled(keys)
if keys._MOUSE_L_DOWN then
-- note we can't clear mouse_lbut here. otherwise we break dragging,
df.global.enabler.mouse_lbut_down = 0
zscreen_inhibit_mouse_l = true
end
if keys._MOUSE_R_DOWN then
df.global.enabler.mouse_rbut_down = 0
df.global.enabler.mouse_rbut = 0
end
end
ZScreen = defclass(ZScreen, Screen)
ZScreen.ATTRS{
2023-02-01 09:46:32 -07:00
defocusable=true,
2023-01-27 15:57:45 -07:00
initial_pause=DEFAULT_NIL,
force_pause=false,
pass_pause=true,
pass_movement_keys=false,
pass_mouse_clicks=true,
}
2023-01-27 15:57:45 -07:00
function ZScreen:preinit(args)
if self.ATTRS.initial_pause == nil then
args.initial_pause = DEFAULT_INITIAL_PAUSE or
(self.ATTRS.pass_mouse_clicks == false)
2023-01-27 15:57:45 -07:00
end
end
function ZScreen:init()
self.saved_pause_state = df.global.pause_state
if self.initial_pause and dfhack.isMapLoaded() then
df.global.pause_state = true
end
self.defocused = false
end
function ZScreen:dismiss()
ZScreen.super.dismiss(self)
if (self.force_pause or self.initial_pause) and dfhack.isMapLoaded() then
-- never go from unpaused to paused, just from paused to unpaused
df.global.pause_state = df.global.pause_state and self.saved_pause_state
end
end
2023-01-29 00:34:23 -07:00
local NO_LOGIC_SCREENS = {
'viewscreen_loadgamest',
'viewscreen_adopt_regionst',
'viewscreen_export_regionst',
'viewscreen_choose_game_typest',
2023-01-28 09:57:24 -07:00
'viewscreen_worldst',
}
2023-01-29 00:34:23 -07:00
for _,v in ipairs(NO_LOGIC_SCREENS) do
if not df[v] then
error('invalid class name: ' .. v)
end
NO_LOGIC_SCREENS[df[v]] = true
end
-- this is necessary for middle-click map scrolling to function
function ZScreen:onIdle()
if self.force_pause and dfhack.isMapLoaded() then
df.global.pause_state = true
end
if self._native and self._native.parent then
2023-01-29 00:34:23 -07:00
local vs_type = dfhack.gui.getDFViewscreen(true)._type
if NO_LOGIC_SCREENS[vs_type] then
self.force_pause = true
2023-01-28 09:57:24 -07:00
self.pass_movement_keys = false
self.pass_mouse_clicks = false
else
self._native.parent:logic()
end
end
end
function ZScreen:render(dc)
self:renderParent()
ZScreen.super.render(self, dc)
end
function ZScreen:hasFocus()
return not self.defocused
and dfhack.gui.getCurViewscreen(true) == self._native
end
function ZScreen:onInput(keys)
local has_mouse = self:isMouseOver()
if not self:hasFocus() then
if has_mouse and
(keys._MOUSE_L_DOWN or keys._MOUSE_R_DOWN or
2023-05-23 16:20:09 -06:00
keys.CONTEXT_SCROLL_UP or keys.CONTEXT_SCROLL_DOWN or
keys.CONTEXT_SCROLL_PAGEUP or keys.CONTEXT_SCROLL_PAGEDOWN) then
self:raise()
else
self:sendInputToParent(keys)
return
end
end
if ZScreen.super.onInput(self, keys) then
markMouseClicksHandled(keys)
return
end
if self.pass_mouse_clicks and keys._MOUSE_L_DOWN and not has_mouse then
2023-02-01 09:46:32 -07:00
self.defocused = self.defocusable
self:sendInputToParent(keys)
return
elseif keys.LEAVESCREEN or keys._MOUSE_R_DOWN then
self:dismiss()
markMouseClicksHandled(keys)
return
else
if zscreen_inhibit_mouse_l then
if keys._MOUSE_L then
return
else
zscreen_inhibit_mouse_l = false
end
end
local passit = self.pass_pause and keys.D_PAUSE
if not passit and self.pass_mouse_clicks then
2023-05-23 16:20:09 -06:00
if keys.CONTEXT_SCROLL_UP or keys.CONTEXT_SCROLL_DOWN or
keys.CONTEXT_SCROLL_PAGEUP or keys.CONTEXT_SCROLL_PAGEDOWN then
passit = true
else
for key in pairs(MOUSE_KEYS) do
if keys[key] then
passit = true
break
end
end
end
end
if not passit and self.pass_movement_keys then
passit = require('gui.dwarfmode').getMapKey(keys)
end
if passit then
self:sendInputToParent(keys)
end
return
end
end
function ZScreen:raise()
if self:isDismissed() or self:hasFocus() then
2023-01-06 12:11:11 -07:00
return self
end
dscreen.raise(self)
self.defocused = false
2023-01-06 12:11:11 -07:00
return self
end
function ZScreen:isMouseOver()
for _,sv in ipairs(self.subviews) do
if sv:getMouseFramePos() then return true end
end
end
2023-01-09 10:35:55 -07:00
local function zscreen_get_any(scr, thing)
if not scr._native or not scr._native.parent then return nil end
return dfhack.gui['getAny'..thing](scr._native.parent)
end
function ZScreen:onGetSelectedUnit()
return zscreen_get_any(self, 'Unit')
end
function ZScreen:onGetSelectedItem()
return zscreen_get_any(self, 'Item')
end
function ZScreen:onGetSelectedBuilding()
return zscreen_get_any(self, 'Building')
end
function ZScreen:onGetSelectedStockpile()
return zscreen_get_any(self, 'Stockpile')
end
function ZScreen:onGetSelectedCivZone()
return zscreen_get_any(self, 'CivZone')
end
2023-01-09 10:35:55 -07:00
function ZScreen:onGetSelectedPlant()
return zscreen_get_any(self, 'Plant')
end
-- convenience subclass for modal dialogs
ZScreenModal = defclass(ZScreenModal, ZScreen)
ZScreenModal.ATTRS{
defocusable = false,
force_pause = true,
pass_pause = false,
pass_movement_keys = false,
pass_mouse_clicks = false,
}
2023-08-14 04:04:38 -06:00
-- Framed screen object
--------------------------
-- Plain grey-colored frame.
-- deprecated
GREY_FRAME = {
frame_pen = to_pen{ ch = ' ', fg = COLOR_BLACK, bg = COLOR_GREY },
title_pen = to_pen{ fg = COLOR_BLACK, bg = COLOR_WHITE },
signature_pen = to_pen{ fg = COLOR_BLACK, bg = COLOR_GREY },
}
-- The boundary used by the pre-steam DF screens.
-- deprecated
BOUNDARY_FRAME = {
frame_pen = to_pen{ ch = 0xDB, fg = COLOR_GREY, bg = COLOR_BLACK },
title_pen = to_pen{ fg = COLOR_BLACK, bg = COLOR_GREY },
signature_pen = to_pen{ fg = COLOR_BLACK, bg = COLOR_GREY },
}
local BASE_FRAME = {
frame_pen = to_pen{ ch=206, fg=COLOR_GREY, bg=COLOR_BLACK },
title_pen = to_pen{ fg=COLOR_BLACK, bg=COLOR_GREY },
inactive_title_pen = to_pen{ fg=COLOR_GREY, bg=COLOR_BLACK },
signature_pen = to_pen{ fg=COLOR_GREY, bg=COLOR_BLACK },
paused_pen = to_pen{fg=COLOR_RED, bg=COLOR_BLACK},
}
2023-08-13 23:17:57 -06:00
local function make_frame(tp, double_line)
local frame = copyall(BASE_FRAME)
frame.t_frame_pen = to_pen{ tile=curry(tp, 2), ch=double_line and 205 or 196, fg=COLOR_GREY, bg=COLOR_BLACK }
frame.l_frame_pen = to_pen{ tile=curry(tp, 8), ch=double_line and 186 or 179, fg=COLOR_GREY, bg=COLOR_BLACK }
frame.b_frame_pen = to_pen{ tile=curry(tp, 16), ch=double_line and 205 or 196, fg=COLOR_GREY, bg=COLOR_BLACK }
frame.r_frame_pen = to_pen{ tile=curry(tp, 10), ch=double_line and 186 or 179, fg=COLOR_GREY, bg=COLOR_BLACK }
frame.lt_frame_pen = to_pen{ tile=curry(tp, 1), ch=double_line and 201 or 218, fg=COLOR_GREY, bg=COLOR_BLACK }
frame.lb_frame_pen = to_pen{ tile=curry(tp, 15), ch=double_line and 200 or 192, fg=COLOR_GREY, bg=COLOR_BLACK }
frame.rt_frame_pen = to_pen{ tile=curry(tp, 3), ch=double_line and 187 or 191, fg=COLOR_GREY, bg=COLOR_BLACK }
frame.rb_frame_pen = to_pen{ tile=curry(tp, 17), ch=double_line and 188 or 217, fg=COLOR_GREY, bg=COLOR_BLACK }
return frame
end
2023-08-13 23:17:57 -06:00
function FRAME_WINDOW(resizable)
2023-08-27 21:58:30 -06:00
local frame = make_frame(textures.tp_border_window, true)
2023-08-13 23:17:57 -06:00
if not resizable then
2023-08-27 21:58:30 -06:00
frame.rb_frame_pen = to_pen{ tile=curry(textures.tp_border_panel, 17), ch=double_line and 188 or 217, fg=COLOR_GREY, bg=COLOR_BLACK }
2023-08-13 23:17:57 -06:00
end
return frame
end
function FRAME_PANEL(resizable)
2023-08-27 21:58:30 -06:00
return make_frame(textures.tp_border_panel, false)
2023-08-13 23:17:57 -06:00
end
function FRAME_MEDIUM(resizable)
2023-08-27 21:58:30 -06:00
return make_frame(textures.tp_border_medium, false)
2023-08-13 23:17:57 -06:00
end
function FRAME_BOLD(resizable)
2023-08-27 21:58:30 -06:00
return make_frame(textures.tp_border_bold, true)
2023-08-13 23:17:57 -06:00
end
function FRAME_INTERIOR(resizable)
2023-08-27 21:58:30 -06:00
local frame = make_frame(textures.tp_border_thin, false)
2023-08-13 23:17:57 -06:00
frame.signature_pen = false
return frame
end
function FRAME_INTERIOR_MEDIUM(resizable)
2023-08-27 21:58:30 -06:00
local frame = make_frame(textures.tp_border_medium, false)
2023-08-13 23:17:57 -06:00
frame.signature_pen = false
return frame
end
2023-01-23 05:03:31 -07:00
-- for compatibility with pre-steam code
2023-05-18 03:22:58 -06:00
GREY_LINE_FRAME = FRAME_PANEL
-- for compatibility with deprecated frame naming scheme
WINDOW_FRAME = FRAME_WINDOW
PANEL_FRAME = FRAME_PANEL
MEDIUM_FRAME = FRAME_MEDIUM
BOLD_FRAME = FRAME_BOLD
INTERIOR_FRAME = FRAME_INTERIOR
INTERIOR_MEDIUM_FRAME = FRAME_INTERIOR_MEDIUM
2023-08-12 07:11:42 -06:00
function paint_frame(dc, rect, style, title, inactive, pause_forced, resizable)
2023-08-13 23:17:57 -06:00
if type(style) == 'function' then
style = style(resizable)
end
local pen = style.frame_pen
local x1,y1,x2,y2 = dc.x1+rect.x1, dc.y1+rect.y1, dc.x1+rect.x2, dc.y1+rect.y2
dscreen.paintTile(style.lt_frame_pen or pen, x1, y1)
dscreen.paintTile(style.rt_frame_pen or pen, x2, y1)
dscreen.paintTile(style.lb_frame_pen or pen, x1, y2)
2023-08-13 23:17:57 -06:00
dscreen.paintTile(style.rb_frame_pen or pen, x2, y2)
dscreen.fillRect(style.t_frame_pen or style.h_frame_pen or pen,x1+1,y1,x2-1,y1)
dscreen.fillRect(style.b_frame_pen or style.h_frame_pen or pen,x1+1,y2,x2-1,y2)
dscreen.fillRect(style.l_frame_pen or style.v_frame_pen or pen,x1,y1+1,x1,y2-1)
dscreen.fillRect(style.r_frame_pen or style.v_frame_pen or pen,x2,y1+1,x2,y2-1)
if style.signature_pen ~= false then
dscreen.paintString(style.signature_pen or style.title_pen or pen,x2-7,y2,"DFHack")
end
if title then
local x = math.max(0,math.floor((x2-x1-3-#title)/2)) + x1
local tstr = ' '..title..' '
if #tstr > x2-x1-1 then
tstr = string.sub(tstr,1,x2-x1-1)
end
dscreen.paintString(inactive and style.inactive_title_pen or style.title_pen or pen,
x, y1, tstr)
end
if pause_forced then
dscreen.paintString(style.paused_pen or style.title_pen or pen,
x1+2, y2, ' PAUSE FORCED ')
end
end
FramedScreen = defclass(FramedScreen, Screen)
FramedScreen.ATTRS{
frame_style = BOUNDARY_FRAME,
frame_title = DEFAULT_NIL,
frame_width = DEFAULT_NIL,
frame_height = DEFAULT_NIL,
frame_inset = 0,
frame_background = CLEAR_PEN,
}
function FramedScreen:getWantedFrameSize()
return self.frame_width, self.frame_height
end
function FramedScreen:computeFrame(parent_rect)
local sw, sh = parent_rect.width, parent_rect.height
2012-10-16 08:33:00 -06:00
local fw, fh = self:getWantedFrameSize(parent_rect)
return compute_frame_body(sw, sh, { w = fw, h = fh }, self.frame_inset, 1, true)
end
function FramedScreen:onRenderFrame(dc, rect)
if rect.wgap <= 0 and rect.hgap <= 0 then
dc:clear()
else
self:renderParent()
dc:fill(rect, self.frame_background)
end
paint_frame(dc,rect,self.frame_style,self.frame_title)
end
-- Inverts the brightness of the color, optionally taking a "bold" parameter,
-- which you should include if you're reading the fg color of a pen.
function invert_color(color, bold)
color = bold and (color + 8) or color
return (color + 8) % 16
end
return _ENV