Extract an abstract View superclass from Screen to handle widget trees.

develop
Alexander Gavrilov 2012-10-15 15:30:00 +04:00
parent adfb307942
commit 33bd8103de
8 changed files with 249 additions and 99 deletions

@ -1727,6 +1727,18 @@ environment by the mandatory init file dfhack.lua:
Returns a table with x, y and z as fields. Returns a table with x, y and z as fields.
* ``same_xyz(a,b)``
Checks if ``a`` and ``b`` have the same x, y and z fields.
* ``get_path_xyz(path,i)``
Returns ``path.x[i], path.y[i], path.z[i]``.
* ``pos2xy(obj)``, ``xy2pos(x,y)``, ``same_xy(a,b)``, ``get_path_xy(a,b)``
Same as above, but for 2D coordinates.
* ``safe_index(obj,index...)`` * ``safe_index(obj,index...)``
Walks a sequence of dereferences, which may be represented by numbers or strings. Walks a sequence of dereferences, which may be represented by numbers or strings.

@ -157,6 +157,14 @@ function xyz2pos(x,y,z)
end end
end end
function same_xyz(a,b)
return a and b and a.x == b.x and a.y == b.y and a.z == b.z
end
function get_path_xyz(path,i)
return path.x[i], path.y[i], path.z[i]
end
function pos2xy(pos) function pos2xy(pos)
if pos then if pos then
local x = pos.x local x = pos.x
@ -174,6 +182,14 @@ function xy2pos(x,y)
end end
end end
function same_xy(a,b)
return a and b and a.x == b.x and a.y == b.y
end
function get_path_xy(path,i)
return path.x[i], path.y[i]
end
function safe_index(obj,idx,...) function safe_index(obj,idx,...)
if obj == nil or idx == nil then if obj == nil or idx == nil then
return nil return nil

@ -50,8 +50,8 @@ function mkdims_wh(x1,y1,w,h)
end end
function inset(rect,dx1,dy1,dx2,dy2) function inset(rect,dx1,dy1,dx2,dy2)
return mkdims_xy( return mkdims_xy(
rect.x1+dx1, rect.y1+dy1, rect.x1+dx1, rect.y1+(dy1 or dx1),
rect.x2-(dx2 or dx1), rect.y2-(dy2 or dy1) rect.x2-(dx2 or dx1), rect.y2-(dy2 or dy1 or dx1)
) )
end end
function is_in_rect(rect,x,y) function is_in_rect(rect,x,y)
@ -68,45 +68,68 @@ local function to_pen(default, pen, bg, bold)
end end
end end
---------------------------- function getKeyDisplay(code)
-- Clipped painter object -- if type(code) == 'string' then
---------------------------- code = df.interface_key[code]
end
return dscreen.getKeyDisplay(code)
end
Painter = defclass(Painter, nil) -----------------------------------
-- Clipped view rectangle object --
-----------------------------------
function Painter:init(args) ViewRect = defclass(ViewRect, nil)
local rect = args.rect or mkdims_wh(0,0,dscreen.getWindowSize())
local crect = args.clip_rect or rect function ViewRect:init(args)
self:assign{ if args.view_rect then
x = rect.x1, y = rect.y1, self:assign(args.view_rect)
x1 = rect.x1, clip_x1 = crect.x1, else
y1 = rect.y1, clip_y1 = crect.y1, local rect = args.rect or mkdims_wh(0,0,dscreen.getWindowSize())
x2 = rect.x2, clip_x2 = crect.x2, local crect = args.clip_rect or rect
y2 = rect.y2, clip_y2 = crect.y2, self:assign{
width = rect.x2-rect.x1+1, x1 = rect.x1, clip_x1 = crect.x1,
height = rect.y2-rect.y1+1, y1 = rect.y1, clip_y1 = crect.y1,
cur_pen = to_pen(nil, args.pen or COLOR_GREY) 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 end
function Painter.new(rect, pen) function ViewRect:isDefunct()
return Painter{ rect = rect, pen = pen } return (self.clip_x1 > self.clip_x2 or self.clip_y1 > self.clip_y2)
end end
function Painter.new_xy(x1,y1,x2,y2,pen) function ViewRect:inClipGlobalXY(x,y)
return Painter{ rect = mkdims_xy(x1,y1,x2,y2), pen = pen } return x >= self.clip_x1 and x <= self.clip_x2
and y >= self.clip_y1 and y <= self.clip_y2
end end
function Painter.new_wh(x,y,w,h,pen) function ViewRect:inClipLocalXY(x,y)
return Painter{ rect = mkdims_wh(x,y,w,h), pen = pen } 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 end
function Painter:isValidPos() function ViewRect:localXY(x,y)
return self.x >= self.clip_x1 and self.x <= self.clip_x2 return x-self.x1, y-self.y1
and self.y >= self.clip_y1 and self.y <= self.clip_y2
end end
function Painter:viewport(x,y,w,h) 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 if type(x) == 'table' then
x,y,w,h = x.x1, x.y1, x.width, x.height x,y,w,h = x.x1, x.y1, x.width, x.height
end end
@ -121,17 +144,57 @@ function Painter:viewport(x,y,w,h)
clip_y1 = math.max(self.clip_y1, y1), clip_y1 = math.max(self.clip_y1, y1),
clip_x2 = math.min(self.clip_x2, x2), clip_x2 = math.min(self.clip_x2, x2),
clip_y2 = math.min(self.clip_y2, y2), clip_y2 = math.min(self.clip_y2, y2),
-- Pen
cur_pen = self.cur_pen
} }
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(nil, args.pen or COLOR_GREY)
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(x,y,w,h)
vp.cur_pen = self.cur_pen
return mkinstance(Painter, vp):seek(0,0) return mkinstance(Painter, vp):seek(0,0)
end end
function Painter:localX() function Painter:cursor()
return self.x - self.x1, self.y - self.y1
end
function Painter:cursorX()
return self.x - self.x1 return self.x - self.x1
end end
function Painter:localY() function Painter:cursorY()
return self.y - self.y1 return self.y - self.y1
end end
@ -219,25 +282,109 @@ function Painter:string(text,pen,...)
end end
function Painter:key(code,pen,bg,...) function Painter:key(code,pen,bg,...)
if type(code) == 'string' then
code = df.interface_key[code]
end
return self:string( return self:string(
dscreen.getKeyDisplay(code), getKeyDisplay(code),
pen or COLOR_LIGHTGREEN, bg or self.cur_pen.bg, ... pen or COLOR_LIGHTGREEN, bg or self.cur_pen.bg, ...
) )
end end
--------------------------
-- Abstract view object --
--------------------------
View = defclass(View)
View.ATTRS {
active = true,
hidden = false,
}
function View:init(args)
self.subviews = {}
end
function View:getWindowSize()
local rect = self.frame_body
return rect.width, rect.height
end
function View:getMousePos()
local rect = self.frame_body
local x,y = dscreen.getMousePos()
if rect and rect:inClipGlobalXY(x,y) then
return rect:localXY(x,y)
end
end
function View:computeFrame(parent_rect)
return mkdims_wh(0,0,parent_rect.width,parent_rect.height)
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
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)
for _,child in ipairs(self.subviews) do
child:updateLayout(frame_body)
end
self:invoke_after('postUpdateLayout',frame_body)
end
function View:renderSubviews(dc)
for _,child in ipairs(self.subviews) do
if not child.hidden then
child:render(dc)
end
end
end
function View:render(dc)
local sub_dc = Painter{
view_rect = self.frame_body,
clip_view = dc
}
self:onRenderBody(sub_dc)
self:renderSubviews(sub_dc)
end
function View:onRenderBody(dc)
end
function View:inputToSubviews(keys)
for _,child in ipairs(self.subviews) do
if child.active and child:onInput(keys) then
return true
end
end
return false
end
function View:onInput(keys)
return self:inputToSubviews(keys)
end
------------------------ ------------------------
-- Base screen object -- -- Base screen object --
------------------------ ------------------------
Screen = defclass(Screen) Screen = defclass(Screen, View)
Screen.text_input_mode = false Screen.text_input_mode = false
function Screen:postinit() function Screen:postinit()
self:updateLayout() self:onResize(dscreen.getWindowSize())
end end
Screen.isDismissed = dscreen.isDismissed Screen.isDismissed = dscreen.isDismissed
@ -254,14 +401,6 @@ function Screen:invalidate()
dscreen.invalidate() dscreen.invalidate()
end end
function Screen:getWindowSize()
return dscreen.getWindowSize()
end
function Screen:getMousePos()
return dscreen.getMousePos()
end
function Screen:renderParent() function Screen:renderParent()
if self._native and self._native.parent then if self._native and self._native.parent then
self._native.parent:render() self._native.parent:render()
@ -290,7 +429,7 @@ function Screen:onAboutToShow()
end end
function Screen:onShow() function Screen:onShow()
self:updateLayout() self:onResize(dscreen.getWindowSize())
end end
function Screen:dismiss() function Screen:dismiss()
@ -306,11 +445,11 @@ function Screen:onDestroy()
end end
function Screen:onResize(w,h) function Screen:onResize(w,h)
self:updateLayout() self:updateLayout(ViewRect{ rect = mkdims_wh(0,0,w,h) })
end end
function Screen:updateLayout() function Screen:onRender()
self:invoke_after('postUpdateLayout') self:render(Painter.new())
end end
------------------------ ------------------------
@ -372,6 +511,7 @@ FramedScreen.ATTRS{
frame_title = DEFAULT_NIL, frame_title = DEFAULT_NIL,
frame_width = DEFAULT_NIL, frame_width = DEFAULT_NIL,
frame_height = DEFAULT_NIL, frame_height = DEFAULT_NIL,
frame_inset = 0,
} }
local function hint_coord(gap,hint) local function hint_coord(gap,hint)
@ -388,52 +528,35 @@ function FramedScreen:getWantedFrameSize()
return self.frame_width, self.frame_height return self.frame_width, self.frame_height
end end
function FramedScreen:updateFrameSize() function FramedScreen:computeFrame(parent_rect)
local sw, sh = dscreen.getWindowSize() local sw, sh = parent_rect.width, parent_rect.height
local iw, ih = sw-2, sh-2 local ins = self.frame_inset
local thickness = 2+ins*2
local iw, ih = sw-thickness, sh-thickness
local fw, fh = self:getWantedFrameSize() local fw, fh = self:getWantedFrameSize()
local width = math.min(fw or iw, iw) local width = math.min(fw or iw, iw)
local height = math.min(fh or ih, ih) local height = math.min(fh or ih, ih)
local gw, gh = iw-width, ih-height local gw, gh = iw-width, ih-height
local x1, y1 = hint_coord(gw,self.frame_xhint), hint_coord(gh,self.frame_yhint) local x1, y1 = hint_coord(gw,self.frame_xhint), hint_coord(gh,self.frame_yhint)
self.frame_rect = mkdims_wh(x1+1,y1+1,width,height)
self.frame_opaque = (gw == 0 and gh == 0) self.frame_opaque = (gw == 0 and gh == 0)
return mkdims_wh(x1,y1,width+thickness,height+thickness),
mkdims_wh(x1+1+ins,y1+1+ins,width,height)
end end
function FramedScreen:postUpdateLayout() function FramedScreen:render(dc)
self:updateFrameSize()
end
function FramedScreen:getWindowSize()
local rect = self.frame_rect
return rect.width, rect.height
end
function FramedScreen:getMousePos()
local rect = self.frame_rect
local x,y = dscreen.getMousePos()
if is_in_rect(rect,x,y) then
return x-rect.x1, y-rect.y1
end
end
function FramedScreen:onRender()
local rect = self.frame_rect local rect = self.frame_rect
local x1,y1,x2,y2 = rect.x1-1, rect.y1-1, rect.x2+1, rect.y2+1 local x1,y1,x2,y2 = rect.x1, rect.y1, rect.x2, rect.y2
if self.frame_opaque then if self.frame_opaque then
dscreen.clear() dc:clear()
else else
self:renderParent() self:renderParent()
dscreen.fillRect(CLEAR_PEN,x1,y1,x2,y2) dc:fill(rect, CLEAR_PEN)
end end
paint_frame(x1,y1,x2,y2,self.frame_style,self.frame_title) paint_frame(x1,y1,x2,y2,self.frame_style,self.frame_title)
self:onRenderBody(Painter.new(rect)) FramedScreen.super.render(self, dc)
end
function FramedScreen:onRenderBody(dc)
end end
return _ENV return _ENV

@ -120,7 +120,7 @@ function InputBox:onRenderBody(dc)
dc:newline(1) dc:newline(1)
dc:pen(self.input_pen or COLOR_LIGHTCYAN) dc:pen(self.input_pen or COLOR_LIGHTCYAN)
dc:fill(1,dc:localY(),dc.width-2,dc:localY()) dc:fill(1,dc:cursorY(),dc.width-2,dc:cursorY())
local cursor = '_' local cursor = '_'
if math.floor(dfhack.getTickCount()/300) % 2 == 0 then if math.floor(dfhack.getTickCount()/300) % 2 == 0 then

@ -253,8 +253,9 @@ end
DwarfOverlay = defclass(DwarfOverlay, gui.Screen) DwarfOverlay = defclass(DwarfOverlay, gui.Screen)
function DwarfOverlay:postUpdateLayout() function DwarfOverlay:updateLayout(parent_rect)
self.df_layout = getPanelLayout() self.df_layout = getPanelLayout()
DwarfOverlay.super.updateLayout(self, parent_rect)
end end
function DwarfOverlay:getViewport(old_vp) function DwarfOverlay:getViewport(old_vp)
@ -352,12 +353,13 @@ end
MenuOverlay = defclass(MenuOverlay, DwarfOverlay) MenuOverlay = defclass(MenuOverlay, DwarfOverlay)
function MenuOverlay:postUpdateLayout() MenuOverlay.ATTRS {
self.frame_rect = self.df_layout.menu frame_inset = 0
end }
MenuOverlay.getWindowSize = gui.FramedScreen.getWindowSize function MenuOverlay:computeFrame(parent_rect)
MenuOverlay.getMousePos = gui.FramedScreen.getMousePos return self.df_layout.menu, gui.inset(self.df_layout.menu, self.frame_inset)
end
function MenuOverlay:onAboutToShow(below) function MenuOverlay:onAboutToShow(below)
MenuOverlay.super.onAboutToShow(self,below) MenuOverlay.super.onAboutToShow(self,below)
@ -368,7 +370,7 @@ function MenuOverlay:onAboutToShow(below)
end end
end end
function MenuOverlay:onRender() function MenuOverlay:render(dc)
self:renderParent() self:renderParent()
local menu = self.df_layout.menu local menu = self.df_layout.menu
@ -379,7 +381,7 @@ function MenuOverlay:onRender()
menu.x1+1, menu.y2+1, "DFHack" menu.x1+1, menu.y2+1, "DFHack"
) )
self:onRenderBody(gui.Painter.new(menu)) MenuOverlay.super.render(self, dc)
end end
end end

@ -7,10 +7,6 @@ local dlg = require 'gui.dialogs'
local tile_attrs = df.tiletype.attrs local tile_attrs = df.tiletype.attrs
function same_xyz(a,b)
return a and b and a.x == b.x and a.y == b.y and a.z == b.z
end
GuidePathUI = defclass(GuidePathUI, guidm.MenuOverlay) GuidePathUI = defclass(GuidePathUI, guidm.MenuOverlay)
GuidePathUI.focus_path = 'guide-path' GuidePathUI.focus_path = 'guide-path'

@ -7,12 +7,13 @@ local text = 'Woohoo, lua viewscreen :)'
local screen = gui.FramedScreen{ local screen = gui.FramedScreen{
frame_style = gui.GREY_LINE_FRAME, frame_style = gui.GREY_LINE_FRAME,
frame_title = 'Hello World', frame_title = 'Hello World',
frame_width = #text+6, frame_width = #text,
frame_height = 3, frame_height = 1,
frame_inset = 1,
} }
function screen:onRenderBody(dc) function screen:onRenderBody(dc)
dc:seek(3,1):string(text, COLOR_LIGHTGREEN) dc:string(text, COLOR_LIGHTGREEN)
end end
function screen:onInput(keys) function screen:onInput(keys)

@ -234,12 +234,12 @@ function SiegeEngine:onRenderBody_main(dc)
if links then if links then
dc:key('CUSTOM_D'):string(": Delete, ") dc:key('CUSTOM_D'):string(": Delete, ")
dc:key('CUSTOM_O'):string(": Zoom"):newline() dc:key('CUSTOM_O'):string(": Zoom"):newline()
self:renderStockpiles(dc, links, bottom-2-dc:localY()) self:renderStockpiles(dc, links, bottom-2-dc:cursorY())
dc:newline():newline() dc:newline():newline()
end end
local prof = self.building:getWorkshopProfile() or {} local prof = self.building:getWorkshopProfile() or {}
dc:seek(1,math.max(dc:localY(),19)) dc:seek(1,math.max(dc:cursorY(),19))
dc:key('CUSTOM_G'):key('CUSTOM_H'):key('CUSTOM_J'):key('CUSTOM_K') dc:key('CUSTOM_G'):key('CUSTOM_H'):key('CUSTOM_J'):key('CUSTOM_K')
dc:string(': ') dc:string(': ')
dc:string(df.skill_rating.attrs[prof.min_level or 0].caption):string('-') dc:string(df.skill_rating.attrs[prof.min_level or 0].caption):string('-')
@ -461,7 +461,7 @@ function SiegeEngine:onRenderBody(dc)
self.mode.render(dc) self.mode.render(dc)
dc:seek(1, math.max(dc:localY(), 21)):pen(COLOR_WHITE) dc:seek(1, math.max(dc:cursorY(), 21)):pen(COLOR_WHITE)
dc:key('LEAVESCREEN'):string(": Back, ") dc:key('LEAVESCREEN'):string(": Back, ")
dc:key('CUSTOM_C'):string(": Recenter") dc:key('CUSTOM_C'):string(": Recenter")
end end