diff options
Diffstat (limited to 'library/lua/gui.lua')
| -rw-r--r-- | library/lua/gui.lua | 400 |
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 |
