diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index b8c1248b3..7f18ce3e6 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -194,16 +194,28 @@ static bool get_int_field(lua_State *L, T *pf, int idx, const char *name, int de return !nil; } +static bool get_char_field(lua_State *L, char *pf, int idx, const char *name, char defval) +{ + lua_getfield(L, idx, name); + + if (lua_type(L, -1) == LUA_TSTRING) + { + *pf = lua_tostring(L, -1)[0]; + lua_pop(L, 1); + return true; + } + else + { + lua_pop(L, 1); + return get_int_field(L, pf, idx, name, defval); + } +} + static void decode_pen(lua_State *L, Pen &pen, int idx) { idx = lua_absindex(L, idx); - lua_getfield(L, idx, "ch"); - if (lua_isstring(L, -1)) - pen.ch = lua_tostring(L, -1)[0]; - else - get_int_field(L, &pen.ch, idx, "ch", 0); - lua_pop(L, 1); + get_char_field(L, &pen.ch, idx, "ch", 0); get_int_field(L, &pen.fg, idx, "fg", 7); get_int_field(L, &pen.bg, idx, "bg", 0); @@ -1104,7 +1116,12 @@ static int screen_paintTile(lua_State *L) int x = luaL_checkint(L, 2); int y = luaL_checkint(L, 3); if (lua_gettop(L) >= 4 && !lua_isnil(L, 4)) - pen.ch = luaL_checkint(L, 4); + { + if (lua_type(L, 4) == LUA_TSTRING) + pen.ch = lua_tostring(L, 4)[0]; + else + pen.ch = luaL_checkint(L, 4); + } if (lua_gettop(L) >= 5 && !lua_isnil(L, 5)) pen.tile = luaL_checkint(L, 5); lua_pushboolean(L, Screen::paintTile(pen, x, y)); diff --git a/library/lua/gui.lua b/library/lua/gui.lua index 5883a2eea..35ae899c0 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -40,12 +40,153 @@ function simulateInput(screen,...) dscreen._doSimulateInput(screen, keys) end -Screen = defclass(Screen, dfhack.screen) +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 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) + ) +end + +function to_pen(pen, default) + if pen == nil then + return default or {} + elseif type(pen) ~= 'table' then + return {fg=pen} + else + return pen + end +end + +---------------------------- +-- Clipped painter object -- +---------------------------- + +Painter = defclass(Painter, nil) + +function Painter.new(rect, pen) + rect = rect or {} + local sw, sh = dscreen.getWindowSize() + local self = mkdims_xy( + math.max(rect.x1 or 0, 0), + math.max(rect.y1 or 0, 0), + math.min(rect.x2 or sw-1, sw-1), + math.min(rect.y2 or sh-1, sh-1) + ) + self.cur_pen = to_pen(pen or COLOR_GREY) + self.x = self.x1 + self.y = self.y1 + return mkinstance(Painter, self) +end -function Screen.new(attrs) - return mkinstance(Screen, attrs) +function Painter:setRect(x1,y1,x2,y2) + local rect = mkdims_xy(x1,y1,x2,y2) + self.x1 = rect.x1 + self.y1 = rect.y1 + self.x2 = rect.x2 + self.y2 = rect.y2 + self.width = rect.width + self.height = rect.height end +function Painter:isValidPos() + return self.x >= self.x1 and self.x <= self.x2 and self.y >= self.y1 and self.y <= self.y2 +end + +function Painter:inset(dx1,dy1,dx2,dy1) + self:setRect( + self.x1+dx1, self.y1+dy1, + self.x2-(dx2 or dx1), self.y2-(dy2 or dy1) + ) + return self +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(pen, self.cur_pen) + return self +end + +function Painter:clear() + dscreen.fillRect(CLEAR_PEN, self.x1, self.y1, self.x2, self.y2) + return self +end + +function Painter:fill(x1,y1,x2,y2,pen) + if type(x1) == 'table' then + x1, y1, x2, y2, pen = x1.x1, x1.y1, x1.x2, x1.y2, x2 + end + x1 = math.max(x1,self.x1) + y1 = math.max(y1,self.y1) + x2 = math.min(x2,self.x2) + y2 = math.min(y2,self.y2) + dscreen.fillRect(to_pen(pen, self.cur_pen),x1,y1,x2,y2) + return self +end + +function Painter:char(char,pen) + if self:isValidPos() then + dscreen.paintTile(to_pen(pen, self.cur_pen), self.x, self.y, char) + end + return self:advance(1, nil) +end + +function Painter:tile(char,tile,pen) + if self:isValidPos() then + dscreen.paintTile(to_pen(pen, self.cur_pen), self.x, self.y, char, tile) + end + return self:advance(1, nil) +end + +function Painter:string(text,pen) + if self.y >= self.y1 and self.y <= self.y2 then + local dx = 0 + if self.x < self.x1 then + dx = self.x1 - self.x + end + local len = #text + if self.x + len - 1 > self.x2 then + len = self.x2 - self.x + 1 + end + if len > dx then + dscreen.paintString( + to_pen(pen, self.cur_pen), + self.x+dx, self.y, + string.sub(text,dx+1,len) + ) + end + end + return self:advance(#text, nil) +end + +------------------------ +-- Base screen object -- +------------------------ + +Screen = defclass(Screen, dfhack.screen) + function Screen:isShown() return self._native ~= nil end @@ -68,6 +209,22 @@ function Screen:inputToParent(...) end end +function Screen:show() + if self._native then + error("This screen is already on display") + end + self:onAboutToShow() + if dscreen.show(self) then + self:onShown() + end +end + +function Screen:onAboutToShow() +end + +function Screen:onShown() +end + function paint_border(x1,y1,x2,y2,color,title) local pen = { ch = ' ', fg = COLOR_BLACK, bg = color } dscreen.fillRect(pen,x1,y1,x2,y1) @@ -92,13 +249,6 @@ local function hint_coord(gap,hint) end 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 Screen:renderFrame(color,title,width,height,xhint,yhint) local sw, sh = dscreen.getWindowSize() local iw, ih = sw-2, sh-2 @@ -116,7 +266,8 @@ function Screen:renderFrame(color,title,width,height,xhint,yhint) end paint_border(x1,y1,x2,y2,color,title) - return x1+1,y1+1,width,height,x2-1,y2-1 + + return Painter.new(mkdims_wh(x1+1,y1+1,width,height)) end return _ENV diff --git a/library/lua/gui/dwarfmode.lua b/library/lua/gui/dwarfmode.lua index 2c62751ec..c10933f0f 100644 --- a/library/lua/gui/dwarfmode.lua +++ b/library/lua/gui/dwarfmode.lua @@ -39,6 +39,14 @@ function getPanelLayout() return rv end +function getCursorPos() + return copyall(df.global.cursor) +end + +function setCursorPos(cursor) + df.global.cursor = cursor +end + function getViewportPos() return { x = df.global.window_x, @@ -47,39 +55,59 @@ function getViewportPos() } end -function getCursorPos() - return copyall(df.global.cursor) -end - -function setCursorPos(cursor) - df.global.cursor = cursor +function clipViewport(view, layout) + local map = df.global.world.map + layout = layout or getPanelLayout() + return { + x = math.max(0, math.min(view.x, map.x_count-layout.map.width)), + y = math.max(0, math.min(view.y, map.y_count-layout.map.height)), + z = math.max(0, math.min(view.z, map.z_count-1)) + } end -function setViewportPos(layout,view) +function setViewportPos(view, layout) local map = df.global.world.map - df.global.window_x = math.max(0, math.min(view.x, map.x_count-layout.map.width)) - df.global.window_y = math.max(0, math.min(view.y, map.y_count-layout.map.height)) - df.global.window_z = math.max(0, math.min(view.z, map.z_count-1)) - return getViewportPos() + layout = layout or getPanelLayout() + local vp = clipViewport(view, layout) + df.global.window_x = vp.x + df.global.window_y = vp.y + df.global.window_z = vp.z + return vp end -function centerViewportOn(layout,target) - local mapsz = layout.map +function centerViewportOn(target, layout) + layout = layout or getPanelLayout() local view = xyz2pos( target.x-math.floor(layout.map.width/2), target.y-math.floor(layout.map.height/2), target.z ) - return setViewportPos(layout, view) + return setViewportPos(view, layout) end function isInViewport(layout,view,target,gap) gap = gap or 0 - return target.x-gap >= view.x and target.x+gap < view.x+layout.map.width - and target.y-gap >= view.y and target.y+gap < view.y+layout.map.height + + local map = df.global.world.map + return math.max(target.x-gap,0) >= view.x + and math.min(target.x+gap,map.x_count-1) < view.x+layout.map.width + and math.max(target.y-gap,0) >= view.y + and math.min(target.y+gap,map.y_count-1) < view.y+layout.map.height and target.z == view.z end +function revealInViewport(target,gap,view,layout) + layout = layout or getPanelLayout() + + if not isInViewport(layout, getViewportPos(), target, gap) then + if view and isInViewport(layout, view, target, gap) then + return setViewportPos(view, layout) + else + return centerViewportOn(target, layout) + end + end +end + DwarfOverlay = defclass(DwarfOverlay, gui.Screen) function DwarfOverlay:updateLayout() @@ -106,14 +134,26 @@ function DwarfOverlay:propagateMoveKeys(keys) end function DwarfOverlay:onIdle() + -- Dwarfmode constantly needs repainting dscreen.invalidate() end -function DwarfOverlay:clearMenu() +function DwarfOverlay:onAboutToShow() + if not df.viewscreen_dwarfmodest:is_instance(dfhack.gui.getCurViewscreen()) then + error("This screen requires the main dwarfmode view") + end +end + +MenuOverlay = defclass(MenuOverlay, DwarfOverlay) + +function MenuOverlay:onRender() + self:renderParent() + self:updateLayout() + local menu = self.df_layout.menu - if not menu then return nil end - dscreen.fillRect(gui.CLEAR_PEN,menu.x1,menu.y1,menu.x2,menu.y2) - return menu.x1,menu.y1,menu.width,menu.height + if menu then + self:onRenderBody(gui.Painter.new(menu)) + end end return _ENV diff --git a/library/lua/utils.lua b/library/lua/utils.lua index 38a1e6c42..009bdf985 100644 --- a/library/lua/utils.lua +++ b/library/lua/utils.lua @@ -361,6 +361,18 @@ function insert_or_update(vector,item,field,cmp) return added,cur,pos end +-- Calls a method with a string temporary +function call_with_string(obj,methodname,...) + return dfhack.with_temp_object( + df.new "string", + function(str,obj,methodname,...) + obj[methodname](obj,str,...) + return str.value + end, + obj,methodname,... + ) +end + -- Ask a yes-no question function prompt_yes_no(msg,default) local prompt = msg diff --git a/scripts/devel/viewscreen.lua b/scripts/devel/viewscreen.lua index c516a7313..a07ea6f37 100644 --- a/scripts/devel/viewscreen.lua +++ b/scripts/devel/viewscreen.lua @@ -2,11 +2,11 @@ local gui = require 'gui' -local screen = gui.Screen.new({ +local screen = mkinstance(gui.Screen, { onRender = function(self) local text = 'Woohoo, lua viewscreen :)' - local x,y,w,h = self:renderFrame(COLOR_GREY,'Hello World',#text+6,3) - self.paintString({fg=COLOR_LIGHTGREEN},x+3,y+1,text) + local dc = self:renderFrame(COLOR_GREY,'Hello World',#text+6,3) + dc:seek(3,1):string(text, {fg=COLOR_LIGHTGREEN}) end, onInput = function(self,keys) if keys and (keys.LEAVESCREEN or keys.SELECT) then diff --git a/scripts/gui/mechanisms.lua b/scripts/gui/mechanisms.lua index b333f4277..f96d8d63b 100644 --- a/scripts/gui/mechanisms.lua +++ b/scripts/gui/mechanisms.lua @@ -1,16 +1,11 @@ -- Shows mechanisms linked to the current building. +local utils = require 'utils' local gui = require 'gui' local guidm = require 'gui.dwarfmode' function getBuildingName(building) - return dfhack.with_temp_object( - df.new "string", - function(str) - building:getName(str) - return str.value - end - ) + return utils.call_with_string(building, 'getName') end function listMechanismLinks(building) @@ -45,53 +40,51 @@ function listMechanismLinks(building) return lst end -MechanismList = defclass(MechanismList, guidm.DwarfOverlay) +MechanismList = defclass(MechanismList, guidm.MenuOverlay) MechanismList.focus_path = 'mechanisms' function MechanismList.new(building) + local links = listMechanismLinks(building) local self = { - old_cursor = guidm.getCursorPos(), - links = listMechanismLinks(building), + links = links, selected = 1 } - self.links[1].viewport = guidm.getViewportPos() - self.links[1].cursor = guidm.getCursorPos() + links[1].viewport = guidm.getViewportPos() + links[1].cursor = guidm.getCursorPos() + if #links <= 1 then + links[1].mode = 'none' + end return mkinstance(MechanismList, self) end local colors = { - self = COLOR_CYAN, trigger = COLOR_MAGENTA, target = COLOR_GREEN + self = COLOR_CYAN, none = COLOR_CYAN, + trigger = COLOR_GREEN, target = COLOR_GREEN } local icons = { - self = 128, trigger = 17, target = 16 + self = 128, none = 63, trigger = 27, target = 26 } -function MechanismList:onRender() - self:renderParent() - self:updateLayout() - local x,y,w,h = self:clearMenu() - - self.paintString({fg=COLOR_WHITE},x+1,y+1,"Mechanism Links") +function MechanismList:onRenderBody(dc) + dc:clear() + dc:seek(1,1):string("Mechanism Links", COLOR_WHITE):newline() for i,v in ipairs(self.links) do local pen = { fg=colors[v.mode], bold = (i == self.selected) } - self.paintTile(pen, x+2, y+2+i, icons[v.mode]) - self.paintString(pen, x+4, y+2+i, v.name) + dc:newline(1):pen(pen):char(icons[v.mode]) + dc:advance(1):string(v.name) end local nlinks = #self.links - local line = y+4+nlinks if nlinks <= 1 then - self.paintString({fg=COLOR_LIGHTRED},x+1,line,"This building has no links") - line = line+2 + dc:newline():newline(1):string("This building has no links", COLOR_LIGHTRED) end - self.paintString({fg=COLOR_LIGHTGREEN},x+1,line,"Esc") - self.paintString({fg=COLOR_WHITE},x+4,line,": Back,") - self.paintString({fg=COLOR_LIGHTGREEN},x+12,line,"Enter") - self.paintString({fg=COLOR_WHITE},x+17,line,": Switch") + dc:newline():newline(1):pen(COLOR_WHITE) + dc:string("Esc", COLOR_LIGHTGREEN):string(": Back, ") + dc:string("Enter", COLOR_LIGHTGREEN):string(": Switch") end function MechanismList:zoomToLink(link) @@ -105,14 +98,7 @@ function MechanismList:zoomToLink(link) end guidm.setCursorPos(cursor) - if not guidm.isInViewport(self.df_layout, self.df_viewport, cursor, 5) then - local vp = link.viewport - if vp then - guidm.setViewportPos(self.df_layout,vp) - else - guidm.centerViewportOn(self.df_layout,cursor) - end - end + guidm.revealInViewport(cursor, 5, link.viewport, self.df_layout) end function MechanismList:changeSelected(delta) @@ -127,10 +113,10 @@ function MechanismList:onInput(keys) elseif keys.STANDARDSCROLL_DOWN or keys.SECONDSCROLL_DOWN then self:changeSelected(1) elseif keys.LEAVESCREEN then + self:dismiss() if self.selected ~= 1 then self:zoomToLink(self.links[1]) end - self:dismiss() elseif keys.SELECT then self:dismiss() end