From 229c5ad61cfa81b16ae1fd00d8f8823f3171b513 Mon Sep 17 00:00:00 2001 From: Hans-Werner Hilse Date: Fri, 28 Nov 2014 20:12:54 +0000 Subject: [PATCH] change setDirty/refresh API See documentation in the code. In short: There is now one single method, setDirty(), that triggers repaints and/or refreshes. All variables in UIManager are gone - at least from an external perspective. Everything is done through setDirty(). This also allows for easier debugging, since all requests come in via function calls. --- frontend/ui/uimanager.lua | 226 ++++++++++++++++++++++++-------------- 1 file changed, 144 insertions(+), 82 deletions(-) diff --git a/frontend/ui/uimanager.lua b/frontend/ui/uimanager.lua index beefb6fe9..b327fc7e9 100644 --- a/frontend/ui/uimanager.lua +++ b/frontend/ui/uimanager.lua @@ -2,26 +2,16 @@ local Device = require("device") local Screen = Device.screen local Input = require("device").input local Event = require("ui/event") +local Geom = require("ui/geometry") local util = require("ffi/util") local DEBUG = require("dbg") local _ = require("gettext") -- there is only one instance of this local UIManager = { - -- force to repaint all the widget is stack, will be reset to false - -- after each ui loop - repaint_all = false, - -- force to do full refresh, will be reset to false - -- after each ui loop - full_refresh = false, - -- force to do partial refresh, will be reset to false - -- after each ui loop - partial_refresh = false, -- trigger a full refresh when counter reaches FULL_REFRESH_COUNT FULL_REFRESH_COUNT = G_reader_settings:readSetting("full_refresh_count") or DRCOUNTMAX, refresh_count = 0, - -- only update specific regions of the screen - update_regions_func = nil, event_handlers = nil, @@ -31,6 +21,8 @@ local UIManager = { _execution_stack_dirty = false, _dirty = {}, _zeromqs = {}, + _refresh_stack = {}, + _refresh_func_stack = {}, } function UIManager:init() @@ -105,7 +97,7 @@ function UIManager:show(widget, x, y) end end -- and schedule it to be painted - self:setDirty(widget) + self:setDirty(widget, "partial") -- tell the widget that it is shown now widget:handleEvent(Event:new("Show")) -- check if this widget disables double tap gesture @@ -135,7 +127,7 @@ function UIManager:close(widget) if dirty then -- schedule remaining widgets to be painted for i = 1, #self._window_stack do - self:setDirty(self._window_stack[i].widget) + self:setDirty(self._window_stack[i].widget, "partial") end end end @@ -176,16 +168,49 @@ function UIManager:unschedule(action) end end --- register a widget to be repainted -function UIManager:setDirty(widget, refresh_type) - -- "auto": request full refresh - -- "full": force full refresh - -- "partial": partial refresh - if not refresh_type then - refresh_type = "auto" - end +--[[ +register a widget to be repainted and enqueue a refresh + +the second parameter (refreshtype) can either specify a refreshtype +(optionally in combination with a refreshregion - which is suggested) +or a function that returns refreshtype AND refreshregion and is called +after painting the widget. + +E.g.: +UIManager:setDirty(self.widget, "partial") +UIManager:setDirty(self.widget, "partial", Geom:new{x=10,y=10,w=100,h=50}) +UIManager:setDirty(self.widget, function() return "ui", self.someelement.dimen end) +--]] +function UIManager:setDirty(widget, refreshtype, refreshregion) if widget then - self._dirty[widget] = refresh_type + if widget == "all" then + -- special case: set all top-level widgets as being "dirty". + for i = 1, #self._window_stack do + self._dirty[self._window_stack[i].widget] = true + end + else + self._dirty[widget] = true + if DEBUG.is_on then + -- when debugging, we check if we get handed a valid widget, + -- which would be a dialog that was previously passed via show() + local found = false + for i = 1, #self._window_stack do + if self._window_stack[i].widget == widget then found = true end + end + if not found then + DEBUG("INFO: invalid widget for setDirty()", debug.traceback()) + end + end + end + end + -- handle refresh information + if not refreshtype then return end + if type(refreshtype) == "function" then + -- callback, will be issued after painting + table.insert(self._refresh_func_stack, refreshtype) + else + -- otherwise, enqueue refresh + self:_refresh(refreshtype, refreshregion) end end @@ -252,7 +277,7 @@ function UIManager:sendEvent(event) end end -function UIManager:checkTasks() +function UIManager:_checkTasks() local now = { util.gettime() } -- check if we have timed events in our queue and search next one @@ -286,36 +311,85 @@ function UIManager:checkTasks() return wait_until, now end +-- precedence of refresh modes: +local refresh_modes = { fast = 1, ui = 2, partial = 3, full = 4 } +-- refresh methods in framebuffer implementation +local refresh_methods = { + fast = "refreshFast", + ui = "refreshUI", + partial = "refreshPartial", + full = "refreshFull", +} + +--[[ +refresh mode comparision + +will return the mode that takes precedence +--]] +local function update_mode(mode1, mode2) + if refresh_modes[mode1] > refresh_modes[mode2] then + return mode1 + else + return mode2 + end +end + +--[[ +enqueue a refresh + +Widgets call this in their paintTo() method in order to notify +UIManager that a certain part of the screen is to be refreshed. + +mode: refresh mode ("full", "partial", "ui", "fast") +region: Rect() that specifies the region to be updated + optional, update will affect whole screen if not specified. + Note that this should be the exception. +--]] +function UIManager:_refresh(mode, region) + -- default mode is partial + mode = mode or "partial" + -- special case: full screen partial update + -- will get promoted every self.FULL_REFRESH_COUNT updates + if not region and mode == "partial" then + self.refresh_count = (self.refresh_count + 1) % self.FULL_REFRESH_COUNT + if self.refresh_count == self.FULL_REFRESH_COUNT - 1 then + DEBUG("promote refresh to full refresh") + mode = "full" + end + end + + -- if no region is specified, define default region + region = region or Geom:new{w=Screen:getWidth(), h=Screen:getHeight()} + + for i = 1, #self._refresh_stack do + -- check for collision with updates that are already enqueued + if region:intersectWith(self._refresh_stack[i].region) then + -- combine both refreshes' regions + local combined = region:combine(self._refresh_stack[i].region) + -- update the mode, if needed + local mode = update_mode(mode, self._refresh_stack[i].mode) + -- remove colliding update + table.remove(self._refresh_stack, i) + -- and try again with combined data + return self:_refresh(mode, combined) + end + end + -- if we hit no (more) collides, enqueue the update + table.insert(self._refresh_stack, {mode = mode, region = region}) +end + -- repaint dirty widgets -function UIManager:repaint() +function UIManager:_repaint() -- flag in which we will record if we did any repaints at all -- will trigger a refresh if set. local dirty = false - -- we use this to record requests for certain refresh types - -- TODO: fix this, see below - local force_full_refresh = self.full_refresh - self.full_refresh = false - - local force_partial_refresh = self.partial_refresh - self.partial_refresh = false - - local force_fast_refresh = false - for _, widget in ipairs(self._window_stack) do - -- paint if repaint_all is request - -- paint also if current widget or any widget underneath is dirty - if self.repaint_all or dirty or self._dirty[widget.widget] then - widget.widget:paintTo(Screen.bb, widget.x, widget.y) - - -- self._dirty[widget.widget] may also be "auto" - if self._dirty[widget.widget] == "full" then - force_full_refresh = true - elseif self._dirty[widget.widget] == "partial" then - force_partial_refresh = true - elseif self._dirty[widget.widget] == "fast" then - force_fast_refresh = true - end + -- paint if current widget or any widget underneath is dirty + if dirty or self._dirty[widget.widget] then + -- pass hint to widget that we got when setting widget dirty + -- the widget can use this to decide which parts should be refreshed + widget.widget:paintTo(Screen.bb, widget.x, widget.y, self._dirty[widget.widget]) -- and remove from list after painting self._dirty[widget.widget] = nil @@ -324,42 +398,30 @@ function UIManager:repaint() dirty = true end end - self.repaint_all = false - if dirty then - -- select proper refresh mode - -- TODO: fix this. We should probably do separate refreshes - -- by regional refreshes (e.g. fast refresh, some partial refreshes) - -- and full-screen full refresh - local refresh - - if force_fast_refresh then - refresh = Screen.refreshFast - elseif force_partial_refresh then - refresh = Screen.refreshPartial - elseif force_full_refresh or self.refresh_count == self.FULL_REFRESH_COUNT - 1 then - refresh = Screen.refreshFull - -- a full refresh will reset the counter which leads to an automatic full refresh - self.refresh_count = 0 - else - -- default - refresh = Screen.refreshPartial - -- increment refresh counter in this case - self.refresh_count = (self.refresh_count + 1) % self.FULL_REFRESH_COUNT - end + -- execute pending refresh functions + for _, refreshfunc in ipairs(self._refresh_func_stack) do + local refreshtype, region = refreshfunc() + if refreshtype then self:_refresh(refreshtype, region) end + end + self._refresh_func_stack = {} + + -- we should have at least one refresh if we did repaint. + -- If we don't, we add one now and print a warning if we + -- are debugging + if dirty and #self._refresh_stack == 0 then + DEBUG("WARNING: no refresh got enqueued. Will do a partial full screen refresh, which might be inefficient") + self:_refresh("partial") + end - if self.update_regions_func then - local update_regions = self.update_regions_func() - for _, update_region in ipairs(update_regions) do - -- in some rare cases update region has 1 pixel offset - refresh(Screen, update_region.x-1, update_region.y-1, - update_region.w+2, update_region.h+2) - end - self.update_regions_func = nil - else - refresh(Screen) - end + -- execute refreshes: + for _, refresh in ipairs(self._refresh_stack) do + DEBUG("triggering refresh", refresh) + Screen[refresh_methods[refresh.mode]](Screen, + refresh.region.x - 1, refresh.region.y - 1, + refresh.region.w + 2, refresh.region.h + 2) end + self._refresh_stack = {} end -- this is the main loop of the UI controller @@ -373,7 +435,7 @@ function UIManager:run() -- that will be honored when calculating the time to wait -- for input events: repeat - wait_until, now = self:checkTasks() + wait_until, now = self:_checkTasks() --DEBUG("---------------------------------------------------") --DEBUG("exec stack", self._execution_stack) @@ -388,7 +450,7 @@ function UIManager:run() return nil end - self:repaint() + self:_repaint() until not self._execution_stack_dirty -- wait for next event