From 33bd8103de43d1a6cb1d8a0f48819e4d753a94ce Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Mon, 15 Oct 2012 15:30:00 +0400 Subject: [PATCH] Extract an abstract View superclass from Screen to handle widget trees. --- Lua API.rst | 12 ++ library/lua/dfhack.lua | 16 ++ library/lua/gui.lua | 283 ++++++++++++++++++++++++---------- library/lua/gui/dialogs.lua | 2 +- library/lua/gui/dwarfmode.lua | 18 ++- scripts/gui/guide-path.lua | 4 - scripts/gui/hello-world.lua | 7 +- scripts/gui/siege-engine.lua | 6 +- 8 files changed, 249 insertions(+), 99 deletions(-) diff --git a/Lua API.rst b/Lua API.rst index 7ad6aec23..bbee8646c 100644 --- a/Lua API.rst +++ b/Lua API.rst @@ -1727,6 +1727,18 @@ environment by the mandatory init file dfhack.lua: 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...)`` Walks a sequence of dereferences, which may be represented by numbers or strings. diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index ce3be5a87..9fc94a025 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -157,6 +157,14 @@ function xyz2pos(x,y,z) 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) if pos then local x = pos.x @@ -174,6 +182,14 @@ function xy2pos(x,y) 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,...) if obj == nil or idx == nil then return nil diff --git a/library/lua/gui.lua b/library/lua/gui.lua index a5e4f7503..4f98e1bde 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -50,8 +50,8 @@ function mkdims_wh(x1,y1,w,h) end function inset(rect,dx1,dy1,dx2,dy2) return mkdims_xy( - rect.x1+dx1, rect.y1+dy1, - rect.x2-(dx2 or dx1), rect.y2-(dy2 or dy1) + rect.x1+dx1, rect.y1+(dy1 or dx1), + rect.x2-(dx2 or dx1), rect.y2-(dy2 or dy1 or dx1) ) end function is_in_rect(rect,x,y) @@ -68,45 +68,68 @@ local function to_pen(default, pen, bg, bold) end end ----------------------------- --- Clipped painter object -- ----------------------------- +function getKeyDisplay(code) + 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) - local rect = args.rect or mkdims_wh(0,0,dscreen.getWindowSize()) - local crect = args.clip_rect or rect - self:assign{ - x = rect.x1, y = rect.y1, - 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, - cur_pen = to_pen(nil, args.pen or COLOR_GREY) - } +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 Painter.new(rect, pen) - return Painter{ rect = rect, pen = pen } +function ViewRect:isDefunct() + return (self.clip_x1 > self.clip_x2 or self.clip_y1 > self.clip_y2) end -function Painter.new_xy(x1,y1,x2,y2,pen) - return Painter{ rect = mkdims_xy(x1,y1,x2,y2), pen = pen } +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 Painter.new_wh(x,y,w,h,pen) - return Painter{ rect = mkdims_wh(x,y,w,h), pen = pen } +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 Painter:isValidPos() - return self.x >= self.clip_x1 and self.x <= self.clip_x2 - and self.y >= self.clip_y1 and self.y <= self.clip_y2 +function ViewRect:localXY(x,y) + return x-self.x1, y-self.y1 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 x,y,w,h = x.x1, x.y1, x.width, x.height end @@ -121,17 +144,57 @@ function Painter:viewport(x,y,w,h) clip_y1 = math.max(self.clip_y1, y1), clip_x2 = math.min(self.clip_x2, x2), 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) 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 end -function Painter:localY() +function Painter:cursorY() return self.y - self.y1 end @@ -219,25 +282,109 @@ function Painter:string(text,pen,...) end function Painter:key(code,pen,bg,...) - if type(code) == 'string' then - code = df.interface_key[code] - end return self:string( - dscreen.getKeyDisplay(code), + getKeyDisplay(code), pen or COLOR_LIGHTGREEN, bg or self.cur_pen.bg, ... ) 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 -- ------------------------ -Screen = defclass(Screen) +Screen = defclass(Screen, View) Screen.text_input_mode = false function Screen:postinit() - self:updateLayout() + self:onResize(dscreen.getWindowSize()) end Screen.isDismissed = dscreen.isDismissed @@ -254,14 +401,6 @@ function Screen:invalidate() dscreen.invalidate() end -function Screen:getWindowSize() - return dscreen.getWindowSize() -end - -function Screen:getMousePos() - return dscreen.getMousePos() -end - function Screen:renderParent() if self._native and self._native.parent then self._native.parent:render() @@ -290,7 +429,7 @@ function Screen:onAboutToShow() end function Screen:onShow() - self:updateLayout() + self:onResize(dscreen.getWindowSize()) end function Screen:dismiss() @@ -306,11 +445,11 @@ function Screen:onDestroy() end function Screen:onResize(w,h) - self:updateLayout() + self:updateLayout(ViewRect{ rect = mkdims_wh(0,0,w,h) }) end -function Screen:updateLayout() - self:invoke_after('postUpdateLayout') +function Screen:onRender() + self:render(Painter.new()) end ------------------------ @@ -372,6 +511,7 @@ FramedScreen.ATTRS{ frame_title = DEFAULT_NIL, frame_width = DEFAULT_NIL, frame_height = DEFAULT_NIL, + frame_inset = 0, } local function hint_coord(gap,hint) @@ -388,52 +528,35 @@ function FramedScreen:getWantedFrameSize() return self.frame_width, self.frame_height end -function FramedScreen:updateFrameSize() - local sw, sh = dscreen.getWindowSize() - local iw, ih = sw-2, sh-2 +function FramedScreen:computeFrame(parent_rect) + local sw, sh = parent_rect.width, parent_rect.height + local ins = self.frame_inset + local thickness = 2+ins*2 + local iw, ih = sw-thickness, sh-thickness local fw, fh = self:getWantedFrameSize() local width = math.min(fw or iw, iw) local height = math.min(fh or ih, ih) local gw, gh = iw-width, ih-height 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) + return mkdims_wh(x1,y1,width+thickness,height+thickness), + mkdims_wh(x1+1+ins,y1+1+ins,width,height) end -function FramedScreen:postUpdateLayout() - 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() +function FramedScreen:render(dc) 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 - dscreen.clear() + dc:clear() else self:renderParent() - dscreen.fillRect(CLEAR_PEN,x1,y1,x2,y2) + dc:fill(rect, CLEAR_PEN) end paint_frame(x1,y1,x2,y2,self.frame_style,self.frame_title) - self:onRenderBody(Painter.new(rect)) -end - -function FramedScreen:onRenderBody(dc) + FramedScreen.super.render(self, dc) end return _ENV diff --git a/library/lua/gui/dialogs.lua b/library/lua/gui/dialogs.lua index 553bb3a4b..75f7349d1 100644 --- a/library/lua/gui/dialogs.lua +++ b/library/lua/gui/dialogs.lua @@ -120,7 +120,7 @@ function InputBox:onRenderBody(dc) dc:newline(1) 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 = '_' if math.floor(dfhack.getTickCount()/300) % 2 == 0 then diff --git a/library/lua/gui/dwarfmode.lua b/library/lua/gui/dwarfmode.lua index 125e71220..4056fbfcc 100644 --- a/library/lua/gui/dwarfmode.lua +++ b/library/lua/gui/dwarfmode.lua @@ -253,8 +253,9 @@ end DwarfOverlay = defclass(DwarfOverlay, gui.Screen) -function DwarfOverlay:postUpdateLayout() +function DwarfOverlay:updateLayout(parent_rect) self.df_layout = getPanelLayout() + DwarfOverlay.super.updateLayout(self, parent_rect) end function DwarfOverlay:getViewport(old_vp) @@ -352,12 +353,13 @@ end MenuOverlay = defclass(MenuOverlay, DwarfOverlay) -function MenuOverlay:postUpdateLayout() - self.frame_rect = self.df_layout.menu -end +MenuOverlay.ATTRS { + frame_inset = 0 +} -MenuOverlay.getWindowSize = gui.FramedScreen.getWindowSize -MenuOverlay.getMousePos = gui.FramedScreen.getMousePos +function MenuOverlay:computeFrame(parent_rect) + return self.df_layout.menu, gui.inset(self.df_layout.menu, self.frame_inset) +end function MenuOverlay:onAboutToShow(below) MenuOverlay.super.onAboutToShow(self,below) @@ -368,7 +370,7 @@ function MenuOverlay:onAboutToShow(below) end end -function MenuOverlay:onRender() +function MenuOverlay:render(dc) self:renderParent() local menu = self.df_layout.menu @@ -379,7 +381,7 @@ function MenuOverlay:onRender() menu.x1+1, menu.y2+1, "DFHack" ) - self:onRenderBody(gui.Painter.new(menu)) + MenuOverlay.super.render(self, dc) end end diff --git a/scripts/gui/guide-path.lua b/scripts/gui/guide-path.lua index f3e27d917..4d32a99c2 100644 --- a/scripts/gui/guide-path.lua +++ b/scripts/gui/guide-path.lua @@ -7,10 +7,6 @@ local dlg = require 'gui.dialogs' 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.focus_path = 'guide-path' diff --git a/scripts/gui/hello-world.lua b/scripts/gui/hello-world.lua index c8cd3bd01..122f2cd38 100644 --- a/scripts/gui/hello-world.lua +++ b/scripts/gui/hello-world.lua @@ -7,12 +7,13 @@ local text = 'Woohoo, lua viewscreen :)' local screen = gui.FramedScreen{ frame_style = gui.GREY_LINE_FRAME, frame_title = 'Hello World', - frame_width = #text+6, - frame_height = 3, + frame_width = #text, + frame_height = 1, + frame_inset = 1, } function screen:onRenderBody(dc) - dc:seek(3,1):string(text, COLOR_LIGHTGREEN) + dc:string(text, COLOR_LIGHTGREEN) end function screen:onInput(keys) diff --git a/scripts/gui/siege-engine.lua b/scripts/gui/siege-engine.lua index c518a9cf8..9a5d76066 100644 --- a/scripts/gui/siege-engine.lua +++ b/scripts/gui/siege-engine.lua @@ -234,12 +234,12 @@ function SiegeEngine:onRenderBody_main(dc) if links then dc:key('CUSTOM_D'):string(": Delete, ") 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() end 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: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) - 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('CUSTOM_C'):string(": Recenter") end