summaryrefslogtreecommitdiff
path: root/library/lua/gui.lua
diff options
context:
space:
mode:
Diffstat (limited to 'library/lua/gui.lua')
-rw-r--r--library/lua/gui.lua400
1 files changed, 400 insertions, 0 deletions
diff --git a/library/lua/gui.lua b/library/lua/gui.lua
new file mode 100644
index 00000000..ac032166
--- /dev/null
+++ b/library/lua/gui.lua
@@ -0,0 +1,400 @@
+-- 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(arg) == '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.new(rect, pen)
+ rect = rect or mkdims_wh(0,0,dscreen.getWindowSize())
+ local self = {
+ x1 = rect.x1, clip_x1 = rect.x1,
+ y1 = rect.y1, clip_y1 = rect.y1,
+ x2 = rect.x2, clip_x2 = rect.x2,
+ y2 = rect.y2, clip_y2 = rect.y2,
+ width = rect.x2-rect.x1+1,
+ height = rect.y2-rect.y1+1,
+ cur_pen = to_pen(nil, pen or COLOR_GREY)
+ }
+ return mkinstance(Painter, self):seek(0,0)
+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)
+ 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.clip_x1)
+ y1 = math.max(y1,self.clip_y1)
+ x2 = math.min(x2,self.clip_x2)
+ y2 = math.min(y2,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:init()
+ self:updateLayout()
+ return self
+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: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.frame_style = BOUNDARY_FRAME
+
+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:updateFrameSize()
+ local sw, sh = dscreen.getWindowSize()
+ local iw, ih = sw-2, sh-2
+ local width = math.min(self.frame_width or iw, iw)
+ local height = math.min(self.frame_height 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