-- Viewscreen implementation utility collection. local _ENV = mkmodule('gui') local dscreen = dfhack.screen USE_GRAPHICS = dscreen.inGraphicsMode() CLEAR_PEN = {ch=32,fg=0,bg=0} 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 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 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 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 to_pen(default, pen, bg, bold) if pen == nil then return default or {} elseif type(pen) ~= 'table' then return {fg=pen,bg=bg,bold=bold} else return pen end end ---------------------------- -- Clipped painter object -- ---------------------------- Painter = defclass(Painter, nil) 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) } end function Painter.new(rect, pen) return Painter{ rect = rect, pen = pen } 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 end function Painter: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), -- Pen cur_pen = self.cur_pen } return mkinstance(Painter, vp):seek(0,0) end function Painter:localX() return self.x - self.x1 end function Painter:localY() 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 = copyall(self.cur_pen) self.cur_pen.fg = fg self.cur_pen.bold = bold if bg then self.cur_pen.bg = bg end 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) 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) 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) 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) ) end end return self:advance(#text, nil) end ------------------------ -- Base screen object -- ------------------------ Screen = defclass(Screen) Screen.text_input_mode = false function Screen:postinit() self:updateLayout() 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: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() else dscreen.clear() end end function Screen:sendInputToParent(...) if self._native and self._native.parent then simulateInput(self._native.parent, ...) end end function Screen:show(below) if self._native then error("This screen is already on display") end self:onAboutToShow(below) if not dscreen.show(self, below) then error('Could not show screen') end end function Screen:onAboutToShow() end function Screen:onShow() self:updateLayout() end function Screen:dismiss() if self._native then dscreen.dismiss(self) end end function Screen:onDismiss() end function Screen:onDestroy() end function Screen:onResize(w,h) self:updateLayout() end function Screen:updateLayout() end ------------------------ -- Framed screen object -- ------------------------ -- Plain grey-colored frame. GREY_FRAME = { frame_pen = { ch = ' ', fg = COLOR_BLACK, bg = COLOR_GREY }, title_pen = { fg = COLOR_BLACK, bg = COLOR_WHITE }, signature_pen = { fg = COLOR_BLACK, bg = COLOR_GREY }, } -- The usual boundary used by the DF screens. Often has fancy pattern in tilesets. BOUNDARY_FRAME = { frame_pen = { ch = 0xDB, fg = COLOR_DARKGREY, bg = COLOR_BLACK }, title_pen = { fg = COLOR_BLACK, bg = COLOR_GREY }, signature_pen = { fg = COLOR_BLACK, bg = COLOR_DARKGREY }, } GREY_LINE_FRAME = { frame_pen = { ch = 206, fg = COLOR_GREY, bg = COLOR_BLACK }, h_frame_pen = { ch = 205, fg = COLOR_GREY, bg = COLOR_BLACK }, v_frame_pen = { ch = 186, fg = COLOR_GREY, bg = COLOR_BLACK }, lt_frame_pen = { ch = 201, fg = COLOR_GREY, bg = COLOR_BLACK }, lb_frame_pen = { ch = 200, fg = COLOR_GREY, bg = COLOR_BLACK }, rt_frame_pen = { ch = 187, fg = COLOR_GREY, bg = COLOR_BLACK }, rb_frame_pen = { ch = 188, fg = COLOR_GREY, bg = COLOR_BLACK }, title_pen = { fg = COLOR_BLACK, bg = COLOR_GREY }, signature_pen = { fg = COLOR_DARKGREY, bg = COLOR_BLACK }, } function paint_frame(x1,y1,x2,y2,style,title) local pen = style.frame_pen 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) 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) dscreen.paintString(style.signature_pen or style.title_pen or pen,x2-7,y2,"DFHack") 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(style.title_pen or pen, x, y1, tstr) end end FramedScreen = defclass(FramedScreen, Screen) FramedScreen.ATTRS{ frame_style = BOUNDARY_FRAME, frame_title = DEFAULT_NIL, frame_width = DEFAULT_NIL, frame_height = DEFAULT_NIL, } local function hint_coord(gap,hint) if hint and hint > 0 then return math.min(hint,gap) elseif hint and hint < 0 then return math.max(0,gap-hint) else return math.floor(gap/2) end end 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 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) end function FramedScreen:updateLayout() 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 x1,y1,x2,y2 = rect.x1-1, rect.y1-1, rect.x2+1, rect.y2+1 if self.frame_opaque then dscreen.clear() else self:renderParent() dscreen.fillRect(CLEAR_PEN,x1,y1,x2,y2) end paint_frame(x1,y1,x2,y2,self.frame_style,self.frame_title) self:onRenderBody(Painter.new(rect)) end function FramedScreen:onRenderBody(dc) end return _ENV