From fe10d0bce5ffc08f5dd9c0af6414c7ea4c79d543 Mon Sep 17 00:00:00 2001 From: NiLuJe Date: Sat, 20 Feb 2021 18:22:48 +0100 Subject: [PATCH] Revamp flash_ui handling, once more, with feeling ;) (#7262) * Simplify flash_ui handling (by handling the unhighlight pre-callback, c.f., #7262 for more details). * UIManager: Handle translucent window-level widgets (and those wrapped in a translucent MovableContainer) properly in setDirty directly, making sure what's *underneath* them gets repainted to avoid alpha layering glitches. (This was previously handled via localized hacks). * Update UIManager's documentation, and format it properly for ldoc parsing, making the HTML docs more useful. * ReaderView: Reinitialize the various page areas when opening a new document, to prevent poisoning from the previous document. * Event: Handle nils in an event's arguments. * CheckButton/RadioButton: Switch to simple inversion to handle highlighting * CheckButton: Make the highlight span the inner frame's width, instead of just the text's width, if possible. * AlphaContainer: Fix & simplify, given the UIManager alpha handling. * MovableContainer: When translucent, cache the canvas bb used for composition. * Avoid spurious refreshes in a few widgets using various dummy *TextWidgets in order to first compute a text height. * KeyValuePage: Avoid floats in size computations. --- doc/Events.md | 13 +- .../apps/reader/modules/readerbookmark.lua | 9 +- frontend/apps/reader/modules/readerview.lua | 12 +- frontend/ui/event.lua | 3 + frontend/ui/geometry.lua | 15 +- frontend/ui/trapper.lua | 2 +- frontend/ui/uimanager.lua | 471 ++++++++++++------ frontend/ui/widget/button.lua | 221 ++++---- frontend/ui/widget/checkbutton.lua | 41 +- .../ui/widget/container/alphacontainer.lua | 69 +-- .../ui/widget/container/movablecontainer.lua | 41 +- frontend/ui/widget/dictquicklookup.lua | 14 +- frontend/ui/widget/doublespinwidget.lua | 9 +- frontend/ui/widget/eventlistener.lua | 4 +- frontend/ui/widget/iconbutton.lua | 69 +-- frontend/ui/widget/inputdialog.lua | 1 + frontend/ui/widget/inputtext.lua | 13 +- frontend/ui/widget/keyvaluepage.lua | 54 +- frontend/ui/widget/menu.lua | 113 ++--- frontend/ui/widget/radiobutton.lua | 40 +- frontend/ui/widget/scrolltextwidget.lua | 53 +- frontend/ui/widget/spinwidget.lua | 9 +- frontend/ui/widget/textboxwidget.lua | 6 + frontend/ui/widget/textviewer.lua | 6 - frontend/ui/widget/touchmenu.lua | 106 ++-- 25 files changed, 708 insertions(+), 686 deletions(-) diff --git a/doc/Events.md b/doc/Events.md index 924441a15..4a347155f 100644 --- a/doc/Events.md +++ b/doc/Events.md @@ -25,12 +25,11 @@ recalculate the view based on the new typesetting. ## Event propagation ## -Most of the UI components is a subclass of -@{ui.widget.container.widgetcontainer|WidgetContainer}. A WidgetContainer is an array that -stores a list of children widgets. +Most UI components are a subclass of @{ui.widget.container.widgetcontainer|WidgetContainer}. +A WidgetContainer is an array that stores a list of children widgets. -When @{ui.widget.container.widgetcontainer:handleEvent|WidgetContainer:handleEvent} is called with a new -event, it will run roughly the following code: +When @{ui.widget.container.widgetcontainer:handleEvent|WidgetContainer:handleEvent} is called with a new event, +it will run roughly the following code: ```lua -- First propagate event to its children @@ -40,8 +39,8 @@ for _, widget in ipairs(self) do return true end end --- If not consumed by children, try consume by itself -return self["on"..event.name](self, unpack(event.args)) +-- If not consumed by children, consume it ourself +return self["on"..event.name](self, unpack(event.args, 1, event.argc)) ``` ## Event system diff --git a/frontend/apps/reader/modules/readerbookmark.lua b/frontend/apps/reader/modules/readerbookmark.lua index 82b666e04..3f35d3fb6 100644 --- a/frontend/apps/reader/modules/readerbookmark.lua +++ b/frontend/apps/reader/modules/readerbookmark.lua @@ -423,13 +423,8 @@ function ReaderBookmark:onShowBookmark() UIManager:close(self.textviewer) end, }, - } - }, - -- Request a full-screen refresh on close, to clear potential flash_ui highlights - close_callback = function() - -- TextViewer does a "partial" on CloseWidget - UIManager:setDirty(nil, "partial") - end, + }, + } } UIManager:show(self.textviewer) return true diff --git a/frontend/apps/reader/modules/readerview.lua b/frontend/apps/reader/modules/readerview.lua index 9386d684c..44f2d9725 100644 --- a/frontend/apps/reader/modules/readerview.lua +++ b/frontend/apps/reader/modules/readerview.lua @@ -59,9 +59,9 @@ local ReaderView = OverlapGroup:extend{ hinting = true, -- visible area within current viewing page - visible_area = Geom:new{x = 0, y = 0}, + visible_area = nil, -- dimen for current viewing page - page_area = Geom:new{}, + page_area = nil, -- dimen for area to dim dim_area = nil, -- has footer @@ -78,8 +78,12 @@ function ReaderView:init() self.view_modules = {} -- fix recalculate from close document pageno self.state.page = nil - -- fix inherited dim_area for following opened documents - self.dim_area = Geom:new{w = 0, h = 0} + + -- Reset the various areas across documents + self.visible_area = Geom:new{x = 0, y = 0, w = 0, h = 0} + self.page_area = Geom:new{x = 0, y = 0, w = 0, h = 0} + self.dim_area = Geom:new{x = 0, y = 0, w = 0, h = 0} + self:addWidgets() self.emitHintPageEvent = function() self.ui:handleEvent(Event:new("HintPage", self.hinting)) diff --git a/frontend/ui/event.lua b/frontend/ui/event.lua index d59cb544e..f7f5f1a24 100644 --- a/frontend/ui/event.lua +++ b/frontend/ui/event.lua @@ -30,6 +30,9 @@ Event:new("GotoPage", 1) function Event:new(name, ...) local o = { handler = "on"..name, + -- Minor trickery to handle nils, c.f., http://lua-users.org/wiki/VarargTheSecondClassCitizen + --- @fixme: Move to table.pack() (which stores the count in the field `n`) here & table.unpack() in @{ui.widget.eventlistener|EventListener} once we build LuaJIT w/ 5.2 compat. + argc = select('#', ...), args = {...} } setmetatable(o, self) diff --git a/frontend/ui/geometry.lua b/frontend/ui/geometry.lua index ba6a11761..ab2791cb2 100644 --- a/frontend/ui/geometry.lua +++ b/frontend/ui/geometry.lua @@ -15,20 +15,23 @@ Some behaviour is defined for dimensions: Geom:new{ w = Screen:scaleBySize(600), h = Screen:scaleBySize(800), } Just use it on simple tables that have x, y and/or w, h -or define your own types using this as a metatable +or define your own types using this as a metatable. + +Where @{ffi.blitbuffer|BlitBuffer} is concerned, a point at (0, 0) means the top-left corner. ]] local Math = require("optmath") --[[-- +Represents a full rectangle (all fields are set), a point (x & y are set), or a dimension (w & h are set). @table Geom ]] local Geom = { - x = 0, - y = 0, - w = 0, - h = 0, + x = 0, -- left origin + y = 0, -- top origin + w = 0, -- width + h = 0, -- height } function Geom:new(o) @@ -418,7 +421,7 @@ end --[[-- Checks if a dimension or rectangle is empty. -@return bool +@treturn bool ]] function Geom:isEmpty() if self.w == 0 or self.h == 0 then diff --git a/frontend/ui/trapper.lua b/frontend/ui/trapper.lua index 0d825e2bc..076cdcd9e 100644 --- a/frontend/ui/trapper.lua +++ b/frontend/ui/trapper.lua @@ -197,7 +197,7 @@ function Trapper:info(text, fast_refresh) self.current_widget:init() self.current_widget.movable:setMovedOffset(orig_moved_offset) local Screen = require("device").screen - self.current_widget:paintTo(Screen.bb, 0,0) + self.current_widget:paintTo(Screen.bb, 0, 0) local d = self.current_widget[1][1].dimen Screen.refreshUI(Screen, d.x, d.y, d.w, d.h) else diff --git a/frontend/ui/uimanager.lua b/frontend/ui/uimanager.lua index d6e07dbe9..29196450b 100644 --- a/frontend/ui/uimanager.lua +++ b/frontend/ui/uimanager.lua @@ -371,16 +371,24 @@ end --[[-- Registers and shows a widget. -Modal widget should be always on top. -For refreshtype & refreshregion see description of setDirty(). +Widgets are registered in a stack, from bottom to top in registration order, +with a few tweaks to handle modals & toasts: +toast widgets are stacked together on top, +then modal widgets are stacked together, and finally come standard widgets. + +If you think about how painting will be handled (also bottom to top), this makes perfect sense ;). + +For more details about refreshtype, refreshregion & refreshdither see the description of `setDirty`. +If refreshtype is omitted, no refresh will be enqueued at this time. + +@param widget a @{ui.widget.widget|widget} object +@string refreshtype `"full"`, `"flashpartial"`, `"flashui"`, `"partial"`, `"ui"`, `"fast"` (optional) +@param refreshregion a rectangle @{ui.geometry.Geom|Geom} object (optional, requires refreshtype to be set) +@int x horizontal screen offset (optional, `0` if omitted) +@int y vertical screen offset (optional, `0` if omitted) +@bool refreshdither `true` if widget requires dithering (optional, requires refreshtype to be set) +@see setDirty ]] ----- @param widget a widget object ----- @param refreshtype "full", "flashpartial", "flashui", "partial", "ui", "fast" ----- @param refreshregion a Geom object ----- @int x ----- @int y ----- @param refreshdither an optional bool ----- @see setDirty function UIManager:show(widget, refreshtype, refreshregion, x, y, refreshdither) if not widget then logger.dbg("widget not exist to be shown") @@ -422,13 +430,18 @@ end --[[-- Unregisters a widget. -For refreshtype & refreshregion see description of setDirty(). +It will be removed from the stack. +Will flag uncovered widgets as dirty. + +For more details about refreshtype, refreshregion & refreshdither see the description of `setDirty`. +If refreshtype is omitted, no extra refresh will be enqueued at this time, leaving only those from the uncovered widgets. + +@param widget a @{ui.widget.widget|widget} object +@string refreshtype `"full"`, `"flashpartial"`, `"flashui"`, `"partial"`, `"ui"`, `"fast"` (optional) +@param refreshregion a rectangle @{ui.geometry.Geom|Geom} object (optional, requires refreshtype to be set) +@bool refreshdither `true` if the refresh requires dithering (optional, requires refreshtype to be set) +@see setDirty ]] ----- @param widget a widget object ----- @param refreshtype "full", "flashpartial", "flashui", "partial", "ui", "fast" ----- @param refreshregion a Geom object ----- @param refreshdither an optional bool ----- @see setDirty function UIManager:close(widget, refreshtype, refreshregion, refreshdither) if not widget then logger.dbg("widget to be closed does not exist") @@ -535,7 +548,15 @@ dbg:guard(UIManager, 'schedule', assert(action ~= nil) end) ---- Schedules task in a certain amount of seconds (fractions allowed) from now. +--[[-- +Schedules a task to be run a certain amount of seconds from now. + +@number seconds scheduling delay in seconds (supports decimal values) +@func action reference to the task to be scheduled (may be anonymous) +@param ... optional arguments passed to action + +@see unschedule +]] function UIManager:scheduleIn(seconds, action, ...) local when = { ffiUtil.gettime() } local s = math.floor(seconds) @@ -553,13 +574,32 @@ dbg:guard(UIManager, 'scheduleIn', assert(seconds >= 0, "Only positive seconds allowed") end) -function UIManager:nextTick(action) - return self:scheduleIn(0, action) +--[[-- +Schedules a task for the next UI tick. + +@func action reference to the task to be scheduled (may be anonymous) +@param ... optional arguments passed to action +@see scheduleIn +]] +function UIManager:nextTick(action, ...) + return self:scheduleIn(0, action, ...) end --- Useful to run UI callbacks ASAP without skipping repaints -function UIManager:tickAfterNext(action) - return self:nextTick(function() self:nextTick(action) end) +--[[-- +Schedules a task to be run two UI ticks from now. + +Useful to run UI callbacks ASAP without skipping repaints. + +@func action reference to the task to be scheduled (may be anonymous) +@param ... optional arguments passed to action +@see nextTick +]] +function UIManager:tickAfterNext(action, ...) + -- Storing varargs is a bit iffy as we don't build LuaJIT w/ 5.2 compat, so we don't have access to table.pack... + -- c.f., http://lua-users.org/wiki/VarargTheSecondClassCitizen + local n = select('#', ...) + local va = {...} + return self:nextTick(function() self:nextTick(action, unpack(va, 1, n)) end) end --[[ -- NOTE: This appears to work *nearly* just as well, but does sometimes go too fast (might depend on kernel HZ & NO_HZ settings?) @@ -568,14 +608,18 @@ function UIManager:tickAfterNext(action) end --]] ---[[-- Unschedules an execution task. +--[[-- +Unschedules a previously scheduled task. In order to unschedule anonymous functions, store a reference. +@func action +@see scheduleIn + @usage self.anonymousFunction = function() self:regularFunction() end -UIManager:scheduleIn(10, self.anonymousFunction) +UIManager:scheduleIn(10.5, self.anonymousFunction) UIManager:unschedule(self.anonymousFunction) ]] function UIManager:unschedule(action) @@ -592,15 +636,15 @@ dbg:guard(UIManager, 'unschedule', function(self, action) assert(action ~= nil) end) --[[-- -Registers a widget to be repainted and enqueues a refresh. +Mark a window-level widget as dirty, enqueuing a repaint & refresh request for that widget, to be processed on the next UI tick. -the second parameter (refreshtype) can either specify a refreshtype +The second parameter (refreshtype) can either specify a refreshtype (optionally in combination with a refreshregion - which is suggested, -and an even more optional refreshdither flag if the content requires dithering) -or a function that returns a refreshtype, refreshregion tuple (or a refreshtype, refreshregion, refreshdither triple) -and is called *after* painting the widget. +and an even more optional refreshdither flag if the content requires dithering); +or a function that returns a refreshtype, refreshregion tuple (or a refreshtype, refreshregion, refreshdither triple), +which will be called *after* painting the widget. This is an interesting distinction, because a widget's geometry, -usually stored in a field named `dimen`, in only computed at painting time (e.g., during `paintTo`). +usually stored in a field named `dimen`, is (generally) only computed at painting time (e.g., during `paintTo`). The TL;DR being: if you already know the region, you can pass everything by value directly, (it'll make for slightly more readable debug logs), but if the region will only be known after the widget has been painted, pass a function. @@ -609,50 +653,60 @@ In practice, since the stack of (both types of) refreshes is optimized into as f and that during the next `_repaint` tick (which is when `paintTo` for dirty widgets happens), this shouldn't change much in the grand scheme of things, but it ought to be noted ;). +See `_repaint` for more details about how the repaint & refresh queues are processed, +and `handleInput` for more details about when those queues are actually drained. +What you should essentially remember is that `setDirty` doesn't actually "do" anything visible on its own. +It doesn't block, and when it returns, nothing new has actually been painted or refreshed. +It just appends stuff to the paint and/or refresh queues. + Here's a quick rundown of what each refreshtype should be used for: -full: high-fidelity flashing refresh (e.g., large images). - Highest quality, but highest latency. - Don't abuse if you only want a flash (in this case, prefer flashpartial or flashui). -partial: medium fidelity refresh (e.g., text on a white background). - Can be promoted to flashing after FULL_REFRESH_COUNT refreshes. - Don't abuse to avoid spurious flashes. -ui: medium fidelity refresh (e.g., mixed content). - Should apply to most UI elements. -fast: low fidelity refresh (e.g., monochrome content). - Should apply to most highlighting effects achieved through inversion. - Note that if your highlighted element contains text, - you might want to keep the unhighlight refresh as "ui" instead, for crisper text. - (Or optimize that refresh away entirely, if you can get away with it). -flashui: like ui, but flashing. - Can be used when showing a UI element for the first time, to avoid ghosting. -flashpartial: like partial, but flashing (and not counting towards flashing promotions). - Can be used when closing an UI element, to avoid ghosting. - You can even drop the region in these cases, to ensure a fullscreen flash. - NOTE: On REAGL devices, "flashpartial" will NOT actually flash (by design). - As such, even onCloseWidget, you might prefer "flashui" in some rare instances. - -NOTE: You'll notice a trend on UI elements that are usually shown *over* some kind of text - of using "ui" onShow & onUpdate, but "partial" onCloseWidget. - This is by design: "partial" is what the reader uses, as it's tailor-made for pure text - over a white background, so this ensures we resume the usual flow of the reader. - The same dynamic is true for their flashing counterparts, in the rare instances we enforce flashes. - Any kind of "partial" refresh *will* count towards a flashing promotion after FULL_REFRESH_COUNT refreshes, - so making sure your stuff only applies to the proper region is key to avoiding spurious large black flashes. - That said, depending on your use case, using "ui" onCloseWidget can be a perfectly valid decision, - and will ensure never seeing a flash because of that widget. - Remember that the FM uses "ui", so, if said widgets are shown over the FM, - prefer using "ui" or "flashui" onCloseWidget. + +* `full`: high-fidelity flashing refresh (e.g., large images). + Highest quality, but highest latency. + Don't abuse if you only want a flash (in this case, prefer `flashui` or `flashpartial`). +* `partial`: medium fidelity refresh (e.g., text on a white background). + Can be promoted to flashing after `FULL_REFRESH_COUNT` refreshes. + Don't abuse to avoid spurious flashes. + In practice, this means this should mostly always be limited to ReaderUI. +* `ui`: medium fidelity refresh (e.g., mixed content). + Should apply to most UI elements. + When in doubt, use this. +* `fast`: low fidelity refresh (e.g., monochrome content). + Should apply to most highlighting effects achieved through inversion. + Note that if your highlighted element contains text, + you might want to keep the unhighlight refresh as `"ui"` instead, for crisper text. + (Or optimize that refresh away entirely, if you can get away with it). +* `flashui`: like `ui`, but flashing. + Can be used when showing a UI element for the first time, or when closing one, to avoid ghosting. +* `flashpartial`: like `partial`, but flashing (and not counting towards flashing promotions). + Can be used when closing an UI element (usually over ReaderUI), to avoid ghosting. + You can even drop the region in these cases, to ensure a fullscreen flash. + NOTE: On REAGL devices, `flashpartial` will NOT actually flash (by design). + As such, even onCloseWidget, you might prefer `flashui` in most instances. + +NOTE: You'll notice a trend on UI elements that are usually shown *over* some kind of text (generally ReaderUI) +of using `"ui"` onShow & onUpdate, but `"partial"` onCloseWidget. +This is by design: `"partial"` is what the reader (ReaderUI) uses, as it's tailor-made for pure text +over a white background, so this ensures we resume the usual flow of the reader. +The same dynamic is true for their flashing counterparts, in the rare instances we enforce flashes. +Any kind of `"partial"` refresh *will* count towards a flashing promotion after `FULL_REFRESH_COUNT` refreshes, +so making sure your stuff only applies to the proper region is key to avoiding spurious large black flashes. +That said, depending on your use case, using `"ui"` onCloseWidget can be a perfectly valid decision, +and will ensure never seeing a flash because of that widget. +Remember that the FM uses `"ui"`, so, if said widgets are shown over the FM, +prefer using `"ui"` or `"flashui"` onCloseWidget. The final parameter (refreshdither) is an optional hint for devices with hardware dithering support that this repaint -could benefit from dithering (i.e., it contains an image). +could benefit from dithering (e.g., because it contains an image). As far as the actual lifecycle of a widget goes, the rules are: + * What you `show`, you `close`. * If you know the dimensions of the widget (or simply of the region you want to refresh), you can pass it directly: - * to show (as show calls setDirty), - * to close (as close will also call setDirty on the remaining dirty and visible widgets, - and will also enqueue a refresh based on that if there are dirty widgets). -* Otherwise, you can use, respectively, the `Show` & `CloseWidget` handlers for that via `setDirty` calls. + * to `show` (as `show` calls `setDirty`), + * to `close` (as `close` will also call `setDirty` on the remaining dirty and visible widgets, + and will also enqueue a refresh based on that if there are dirty widgets). +* Otherwise, you can use, respectively, a widget's `Show` & `CloseWidget` handlers for that via `setDirty` calls. This can also be useful if *child* widgets have specific needs (e.g., flashing, dithering) that they want to inject in the refresh queue. * Remember that events propagate children first (in array order, starting at the top), and that if *any* event handler returns true, the propagation of that specific event for this widget tree stops *immediately*. @@ -660,9 +714,12 @@ As far as the actual lifecycle of a widget goes, the rules are: you generally *don't* want to return true in `Show` or `CloseWidget` handlers). * If any widget requires freeing non-Lua resources (e.g., FFI/C), having a `free` method called from its `CloseWidget` handler is ideal: this'll ensure that *any* widget including it will be sure that resources are freed when it (or its parent) are closed. -* Note that there *is* a `Close` event, but it is *only* broadcast (e.g., sent to every widget in the window stack; - the same rules about propagation apply, but only per *window-level widget*) at poweroff/reboot, so, - refrain from implementing custom onClose methods if that's not their intended purpose ;). +* Note that there *is* a `Close` event, but it has very specific use-cases, generally involving *programmatically* `close`ing a `show`n widget: + * It is broadcast (e.g., sent to every widget in the window stack; the same rules about propagation apply, but only per *window-level widget*) + at poweroff/reboot. + * It can also be used as a keypress handler by @{ui.widget.container.inputcontainer|InputContainer}, generally bound to the Back key. + +Please refrain from implementing custom `onClose` methods if that's not their intended purpose ;). On the subject of widgets and child widgets, you might have noticed an unspoken convention across the codebase of widgets having a field called `show_parent`. @@ -670,23 +727,23 @@ Since handling this is entirely at the programmer's behest, here's how we usuall Basically, we cascade a field named `show_parent` to every child widget that matter (e.g., those that serve an UI purpose, as opposed to, say, a container). This ensures that every subwidget can reference its actual parent -(ideally, all the way to the window-level widget it belongs to, i.e., the one that was passed to UIManager:show, hence the name ;)), -to, among other things, flag the right widget as setDirty (c.f., those pesky debug warnings when that's done wrong ;p) when they want to request a repaint. -This is why you often see stuff doing, when instantiating a new widget, FancyWidget:new{ show_parent = self.show_parent or self }; +(ideally, all the way to the window-level widget it belongs to, i.e., the one that was passed to `show`, hence the name ;)), +to, among other things, flag the right widget for repaint via `setDirty` (c.f., those pesky debug warnings when that's done wrong ;p) when they want to request a repaint. +This is why you often see stuff doing, when instantiating a new widget, `FancyWidget:new{ show_parent = self.show_parent or self }`; meaning, if I'm already a subwidget, cascade my parent, otherwise, it means I'm a window-level widget, so cascade myself as that widget's parent ;). Another convention (that a few things rely on) is naming a (persistent) MovableContainer wrapping a full widget `movable`, accessible as an instance field. -This is useful when it's used for transparency purposes, which, e.g., Button relies on to handle highlighting inside a transparent widget properly, +This is useful when it's used for transparency purposes, which, e.g., `setDirty` and @{ui.widget.button|Button} rely on to handle updating translucent widgets properly, by checking if self.show_parent.movable exists and is currently translucent ;). -When I mentioned passing the *right* widget to `setDirty` earlier, what I meant is that setDirty will only actually flag a widget for repaint +When I mentioned passing the *right* widget to `setDirty` earlier, what I meant is that `setDirty` will only actually flag a widget for repaint *if* that widget is a window-level widget (that is, a widget that was passed to `show` earlier and hasn't been `close`'d yet), -hence the self.show_parent convention detailed above to get at the proper widget from within a subwidget ;). +hence the `self.show_parent` convention detailed above to get at the proper widget from within a subwidget ;). Otherwise, you'll notice in debug mode that a debug guard will shout at you if that contract is broken, and what happens in practice is the same thing as if an explicit `nil` were passed: no widgets will actually be flagged for repaint, and only the *refresh* matching the requested region *will* be enqueued. -This is why you'll find a number of valid use-cases for passing a nil here, when you *just* want a screen refresh without a repaint :). -The string "all" is also accepted in place of a widget, and will do the obvious thing: flag the *full* window stack, bottom to top, for repaint, +This is why you'll find a number of valid use-cases for passing a `nil` here, when you *just* want a screen refresh without a repaint :). +The string `"all"` is also accepted in place of a widget, and will do the obvious thing: flag the *full* window stack, bottom to top, for repaint, while still honoring the refresh region (e.g., this doesn't enforce a full-screen refresh). @usage @@ -695,11 +752,11 @@ 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) ---]] ----- @param widget a window-level widget object, "all", or nil ----- @param refreshtype "full", "flashpartial", "flashui", "partial", "ui", "fast" ----- @param refreshregion an optional Geom object ----- @param refreshdither an optional bool +@param widget a window-level widget object, `"all"`, or `nil` +@param refreshtype `"full"`, `"flashpartial"`, `"flashui"`, `"partial"`, `"ui"`, `"fast"` (or a lambda, see description above) +@param refreshregion a rectangle @{ui.geometry.Geom|Geom} object (optional, omitting it means the region will cover the full screen) +@bool refreshdither `true` if widget requires dithering (optional) +]] function UIManager:setDirty(widget, refreshtype, refreshregion, refreshdither) if widget then if widget == "all" then @@ -715,11 +772,34 @@ function UIManager:setDirty(widget, refreshtype, refreshregion, refreshdither) end end elseif not widget.invisible then - -- We only ever check the dirty flag on top-level widgets, so only set it there! - -- NOTE: Enable verbose debug to catch misbehaving widgets via our post-guard. - for i = 1, #self._window_stack do + -- NOTE: If our widget is translucent, or belongs to a translucent MovableContainer, + -- we'll want to flag everything below it as dirty, too, + -- because doing transparency right requires having an up to date background against which to blend. + -- (The typecheck is because some widgets use an alpha boolean trap for internal alpha handling (e.g., ImageWidget)). + local handle_alpha = false + -- NOTE: We only ever check the dirty flag on top-level widgets, so only set it there! + -- Enable verbose debug to catch misbehaving widgets via our post-guard. + for i = #self._window_stack, 1, -1 do + if handle_alpha then + self._dirty[self._window_stack[i].widget] = true + logger.dbg("setDirty: Marking as dirty widget:", self._window_stack[i].widget.name or self._window_stack[i].widget.id or tostring(self._window_stack[i].widget), "because it's below translucent widget:", widget.name or widget.id or tostring(widget)) + -- Stop flagging widgets at the uppermost one that covers the full screen + if self._window_stack[i].widget.covers_fullscreen then + break + end + end + if self._window_stack[i].widget == widget then self._dirty[widget] = true + + -- We've got a match, now check if it's translucent... + handle_alpha = (widget.alpha and type(widget.alpha) == "number" and widget.alpha < 1 and widget.alpha > 0) + or (widget.movable and widget.movable.alpha and widget.movable.alpha < 1 and widget.movable.alpha > 0) + -- We shouldn't be seeing the same widget at two different spots in the stack, so, we're done, + -- except when we need to keep looping to flag widgets below us in order to handle a translucent widget... + if not handle_alpha then + break + end end end -- Again, if it's flagged as dithered, honor that @@ -731,11 +811,13 @@ function UIManager:setDirty(widget, refreshtype, refreshregion, refreshdither) -- Another special case: if we did NOT specify a widget, but requested a full refresh nonetheless (i.e., a diagonal swipe), -- we'll want to check the window stack in order to honor dithering... if refreshtype == "full" then - for i = 1, #self._window_stack do + for i = #self._window_stack, 1, -1 do -- If any of 'em were dithered, honor their dithering hint if self._window_stack[i].widget.dithered then logger.dbg("setDirty full on no specific widget: found a dithered widget, infecting the refresh queue") refreshdither = true + -- One is enough ;) + break end end end @@ -745,8 +827,9 @@ function UIManager:setDirty(widget, refreshtype, refreshregion, refreshdither) -- callback, will be issued after painting table.insert(self._refresh_func_stack, refreshtype) if dbg.is_on then - --- @fixme We can't consume the return values of refreshtype by running it, because for a reason that is beyond me (scoping? gc?), that renders it useless later, meaning we then enqueue refreshes with bogus arguments... - -- Thankfully, we can track them in _refresh()'s logging very soon after that... + -- NOTE: It's too early to tell what the function will return (especially the region), because the widget hasn't been painted yet. + -- Consuming the lambda now also appears have nasty side-effects that render it useless later, subtly breaking a whole lot of things... + -- Thankfully, we can track them in _refresh()'s logging very soon after that... logger.dbg("setDirty via a func from widget", widget and (widget.name or widget.id or tostring(widget)) or "nil") end else @@ -765,22 +848,28 @@ dbg:guard(UIManager, 'setDirty', nil, function(self, widget, refreshtype, refreshregion, refreshdither) if not widget or widget == "all" then return end - -- when debugging, we check if we get handed a valid widget, - -- which would be a dialog that was previously passed via show() + -- when debugging, we check if we were handed a valid window-level widget, + -- which would be a widget that was previously passed to `show`. local found = false for i = 1, #self._window_stack do - if self._window_stack[i].widget == widget then found = true end + if self._window_stack[i].widget == widget then + found = true + break + end end if not found then dbg:v("INFO: invalid widget for setDirty()", debug.traceback()) end end) --- Clear the full repaint & refreshes queues. --- NOTE: Beware! This doesn't take any prisonners! --- You shouldn't have to resort to this unless in very specific circumstances! --- plugins/coverbrowser.koplugin/covermenu.lua building a franken-menu out of buttondialogtitle & buttondialog --- and wanting to avoid inheriting their original paint/refresh cycle being a prime example. +--[[-- +Clear the full repaint & refresh queues. + +NOTE: Beware! This doesn't take any prisonners! +You shouldn't have to resort to this unless in very specific circumstances! +plugins/coverbrowser.koplugin/covermenu.lua building a franken-menu out of buttondialogtitle & buttondialog +and wanting to avoid inheriting their original paint/refresh cycle being a prime example. +--]] function UIManager:clearRenderStack() logger.dbg("clearRenderStack: Clearing the full render stack!") self._dirty = {} @@ -801,9 +890,15 @@ function UIManager:removeZMQ(zeromq) end end ---- Sets full refresh rate for e-ink screen. --- --- Also makes the refresh rate persistent in global reader settings. +--[[-- +Sets the full refresh rate for e-ink screens (`FULL_REFRESH_COUNT`). + +This is the amount of `"partial"` refreshes before the next one gets promoted to `"full"`. + +Also makes the refresh rate persistent in global reader settings. + +@see setDirty +--]] function UIManager:setRefreshRate(rate, night_rate) logger.dbg("set screen full refresh rate", rate, night_rate) @@ -825,11 +920,12 @@ function UIManager:setRefreshRate(rate, night_rate) end end ---- Gets full refresh rate for e-ink screen. +--- Returns the full refresh rate for e-ink screens (`FULL_REFRESH_COUNT`). function UIManager:getRefreshRate() return G_reader_settings:readSetting("full_refresh_count") or DEFAULT_FULL_REFRESH_COUNT, G_reader_settings:readSetting("night_full_refresh_count") or G_reader_settings:readSetting("full_refresh_count") or DEFAULT_FULL_REFRESH_COUNT end +--- Toggles Night Mode (i.e., inverted rendering). function UIManager:ToggleNightMode(night_mode) if night_mode then self.FULL_REFRESH_COUNT = G_reader_settings:readSetting("night_full_refresh_count") or G_reader_settings:readSetting("full_refresh_count") or DEFAULT_FULL_REFRESH_COUNT @@ -850,9 +946,13 @@ function UIManager:getTopWidget() return top.widget end ---- Get the *second* topmost widget, if there is one (name if possible, ref otherwise). ---- Useful when VirtualKeyboard is involved, as it *always* steals the top spot ;). ---- NOTE: Will skip over VirtualKeyboard instances, plural, in case there are multiple (because, apparently, we can do that.. ugh). +--[[-- +Get the *second* topmost widget, if there is one (name if possible, ref otherwise). + +Useful when VirtualKeyboard is involved, as it *always* steals the top spot ;). + +NOTE: Will skip over VirtualKeyboard instances, plural, in case there are multiple (because, apparently, we can do that.. ugh). +--]] function UIManager:getSecondTopmostWidget() if #self._window_stack <= 1 then -- Not enough widgets in the stack, bye! @@ -881,7 +981,7 @@ function UIManager:getSecondTopmostWidget() return nil end ---- Check if a widget is still in the window stack, or is a subwidget of a widget still in the window stack +--- Check if a widget is still in the window stack, or is a subwidget of a widget still in the window stack. function UIManager:isSubwidgetShown(widget, max_depth) for i = #self._window_stack, 1, -1 do local matched, depth = util.arrayReferences(self._window_stack[i].widget, widget, max_depth) @@ -892,7 +992,7 @@ function UIManager:isSubwidgetShown(widget, max_depth) return false end ---- Same, but only check window-level widgets (e.g., what's directly registered in the window stack), don't recurse +--- Same as `isSubwidgetShown`, but only check window-level widgets (e.g., what's directly registered in the window stack), don't recurse. function UIManager:isWidgetShown(widget) for i = #self._window_stack, 1, -1 do if self._window_stack[i].widget == widget then @@ -902,7 +1002,11 @@ function UIManager:isWidgetShown(widget) return false end --- Returns the region of the previous refresh +--[[-- +Returns the region of the previous refresh. + +@return a rectangle @{ui.geometry.Geom|Geom} object +]] function UIManager:getPreviousRefreshRegion() return self._last_refresh_region end @@ -930,7 +1034,11 @@ function UIManager:quit() end end ---- Request events to be ignored for some duration +--[[-- +Request all @{ui.event.Event|Event}s to be ignored for some duration. + +@param set_or_seconds either `true`, in which case a platform-specific delay is chosen, or a duration in seconds (***int***). +]] function UIManager:discardEvents(set_or_seconds) if not set_or_seconds then -- remove any previously set self._discard_events_till = nil @@ -959,7 +1067,11 @@ function UIManager:discardEvents(set_or_seconds) self._discard_events_till = now_us + usecs end ---- Transmits an event to an active widget. +--[[-- +Transmits an @{ui.event.Event|Event} to active widgets. + +@param event an @{ui.event.Event|Event} object +]] function UIManager:sendEvent(event) if #self._window_stack == 0 then return end @@ -995,14 +1107,14 @@ function UIManager:sendEvent(event) end end - -- if the event is not consumed, active widgets (from top to bottom) can - -- access it. NOTE: _window_stack can shrink on close event + -- if the event is not consumed, active widgets (from top to bottom) can access it. + -- NOTE: _window_stack can shrink when widgets are closed (CloseWidget & Close events). local checked_widgets = {top_widget} for i = #self._window_stack, 1, -1 do local widget = self._window_stack[i] if checked_widgets[widget] == nil then -- active widgets has precedence to handle this event - -- Note: ReaderUI currently only has one active_widget: readerscreenshot + -- NOTE: While FileManager only has a single (screenshotter), ReaderUI has many active_widgets (each ReaderUI module gets added to the list). if widget.widget.active_widgets then checked_widgets[widget] = true for _, active_widget in ipairs(widget.widget.active_widgets) do @@ -1011,8 +1123,7 @@ function UIManager:sendEvent(event) end if widget.widget.is_always_active then -- active widgets will handle this event - -- Note: is_always_active widgets currently are widgets that want to show a keyboard - -- and readerconfig + -- NOTE: is_always_active widgets currently are widgets that want to show a VirtualKeyboard or listen to Dispatcher events checked_widgets[widget] = true if widget.widget:handleEvent(event) then return end end @@ -1020,7 +1131,11 @@ function UIManager:sendEvent(event) end end ---- Transmits an event to all registered widgets. +--[[-- +Transmits an @{ui.event.Event|Event} to all registered widgets. + +@param event an @{ui.event.Event|Event} object +]] function UIManager:broadcastEvent(event) -- the widget's event handler might close widgets in which case -- a simple iterator like ipairs would skip over some entries @@ -1092,7 +1207,7 @@ local refresh_methods = { Compares refresh mode. Will return the mode that takes precedence. ---]] +]] local function update_mode(mode1, mode2) if refresh_modes[mode1] > refresh_modes[mode2] then logger.dbg("update_mode: Update refresh mode", mode2, "to", mode1) @@ -1106,7 +1221,7 @@ end Compares dither hints. Dither always wins. ---]] +]] local function update_dither(dither1, dither2) if dither1 and not dither2 then logger.dbg("update_dither: Update dither hint", dither2, "to", dither1) @@ -1119,19 +1234,21 @@ end --[[-- Enqueues a refresh. -Widgets call this in their paintTo() method in order to notify +Widgets call this in their `paintTo()` method in order to notify UIManager that a certain part of the screen is to be refreshed. -@param mode - refresh mode ("full", "flashpartial", "flashui", "partial", "ui", "fast") +@string mode + refresh mode (`"full"`, `"flashpartial"`, `"flashui"`, `"partial"`, `"ui"`, `"fast"`) @param region - Rect() that specifies the region to be updated - optional, update will affect whole screen if not specified. + A rectangle @{ui.geometry.Geom|Geom} object that specifies the region to be updated. + Optional, update will affect whole screen if not specified. Note that this should be the exception. -@param dither - Bool, a hint to request hardware dithering (if supported) - optional, no dithering requested if not specified or not supported. ---]] +@bool dither + A hint to request hardware dithering (if supported). + Optional, no dithering requested if not specified or not supported. + +@local Not to be used outside of UIManager! +]] function UIManager:_refresh(mode, region, dither) if not mode then -- If we're trying to float a dither hint up from a lower widget after a close, mode might be nil... @@ -1139,6 +1256,8 @@ function UIManager:_refresh(mode, region, dither) if dither then mode = "ui" else + -- Otherwise, this is most likely from a `show` or `close` that wasn't passed specific refresh details, + -- (which is the vast majority of them), in which case we drop it to avoid enqueuing a useless full-screen refresh. return end end @@ -1183,7 +1302,7 @@ function UIManager:_refresh(mode, region, dither) self.refresh_counted = true end - -- if no region is specified, define default region + -- if no region is specified, use the screen's dimensions region = region or Geom:new{w=Screen:getWidth(), h=Screen:getHeight()} -- if no dithering hint was specified, don't request dithering @@ -1195,7 +1314,8 @@ function UIManager:_refresh(mode, region, dither) -- as well as a few actually effective merges -- (e.g., the disappearance of a selection HL with the following menu update). for i = 1, #self._refresh_stack do - -- check for collision with refreshes that are already enqueued + -- Check for collision with refreshes that are already enqueued + -- NOTE: intersect *means* intersect: we won't merge edge-to-edge regions (but the EPDC probably will). if region:intersectWith(self._refresh_stack[i].region) then -- combine both refreshes' regions local combined = region:combine(self._refresh_stack[i].region) @@ -1215,8 +1335,16 @@ function UIManager:_refresh(mode, region, dither) table.insert(self._refresh_stack, {mode = mode, region = region, dither = dither}) end +--[[-- +Repaints dirty widgets. + +This will also drain the refresh queue, effectively refreshing the screen region(s) matching those freshly repainted widgets. + +There may be refreshes enqueued without any widgets needing to be repainted (c.f., `setDirty`'s behavior when passed a `nil` widget), +in which case, nothing is repainted, but the refreshes are still drained and executed. ---- Repaints dirty widgets. +@local Not to be used outside of UIManager! +--]] function UIManager:_repaint() -- flag in which we will record if we did any repaints at all -- will trigger a refresh if set. @@ -1255,12 +1383,15 @@ function UIManager:_repaint() -- the widget can use this to decide which parts should be refreshed logger.dbg("painting widget:", widget.widget.name or widget.widget.id or tostring(widget)) Screen:beforePaint() + -- NOTE: Nothing actually seems to use the final argument? + -- Could be used by widgets to know whether they're being repainted because they're actually dirty (it's true), + -- or because something below them was (it's nil). widget.widget:paintTo(Screen.bb, widget.x, widget.y, self._dirty[widget.widget]) -- and remove from list after painting self._dirty[widget.widget] = nil - -- trigger repaint + -- trigger a repaint for every widget above us, too dirty = true -- if any of 'em were dithered, we'll want to dither the final refresh @@ -1313,17 +1444,37 @@ function UIManager:_repaint() self.refresh_counted = false end +--- Explicitly drain the paint & refresh queues *now*, instead of waiting for the next UI tick. function UIManager:forceRePaint() self:_repaint() end +--[[-- +Ask the EPDC to *block* until our previous refresh ioctl has completed. + +This interacts sanely with the existing low-level handling of this in `framebuffer_mxcfb` +(i.e., it doesn't even try to wait for a marker that fb has already waited for, and vice-versa). + +Will return immediately if it has already completed. + +If the device isn't a Linux + MXCFB device, this is a NOP. +]] function UIManager:waitForVSync() Screen:refreshWaitForLast() end --- Used to repaint a specific sub-widget that isn't on the _window_stack itself --- Useful to avoid repainting a complex widget when we just want to invert an icon, for instance. --- No safety checks on x & y *by design*. I want this to blow up if used wrong. +--[[-- +Used to repaint a specific sub-widget that isn't on the `_window_stack` itself. + +Useful to avoid repainting a complex widget when we just want to invert an icon, for instance. +No safety checks on x & y *by design*. I want this to blow up if used wrong. + +This is an explicit repaint *now*: it bypasses and ignores the paint queue (unlike `setDirty`). + +@param widget a @{ui.widget.widget|widget} object +@int x left origin of widget (in the Screen buffer, e.g., `widget.dimen.x`) +@int y top origin of widget (in the Screen buffer, e.g., `widget.dimen.y`) +]] function UIManager:widgetRepaint(widget, x, y) if not widget then return end @@ -1331,7 +1482,16 @@ function UIManager:widgetRepaint(widget, x, y) widget:paintTo(Screen.bb, x, y) end --- Same idea, but does a simple invertRect, without actually repainting anything +--[[-- +Same idea as `widgetRepaint`, but does a simple `bb:invertRect` on the Screen buffer, without actually going through the widget's `paintTo` method. + +@param widget a @{ui.widget.widget|widget} object +@int x left origin of the rectangle to invert (in the Screen buffer, e.g., `widget.dimen.x`) +@int y top origin of the rectangle (in the Screen buffer, e.g., `widget.dimen.y`) +@int w width of the rectangle (optional, will use `widget.dimen.w` like `paintTo` would if omitted) +@int h height of the rectangle (optional, will use `widget.dimen.h` like `paintTo` would if omitted) +@see widgetRepaint +--]] function UIManager:widgetInvert(widget, x, y, w, h) if not widget then return end @@ -1457,9 +1617,11 @@ function UIManager:initLooper() end end --- this is the main loop of the UI controller --- it is intended to manage input events and delegate --- them to dialogs +--[[-- +This is the main loop of the UI controller. + +It is intended to manage input events and delegate them to dialogs. +--]] function UIManager:run() self._running = true self:initLooper() @@ -1483,17 +1645,18 @@ function UIManager:runForever() return self:run() end --- The common operations should be performed before suspending the device. Ditto. +-- The common operations that should be performed before suspending the device. function UIManager:_beforeSuspend() self:flushSettings() self:broadcastEvent(Event:new("Suspend")) end --- The common operations should be performed after resuming the device. Ditto. +-- The common operations that should be performed after resuming the device. function UIManager:_afterResume() self:broadcastEvent(Event:new("Resume")) end +-- The common operations that should be performed when the device is plugged to a power source. function UIManager:_beforeCharging() if G_reader_settings:nilOrTrue("enable_charging_led") then Device:toggleChargingLED(true) @@ -1501,6 +1664,7 @@ function UIManager:_beforeCharging() self:broadcastEvent(Event:new("Charging")) end +-- The common operations that should be performed when the device is unplugged from a power source. function UIManager:_afterNotCharging() if G_reader_settings:nilOrTrue("enable_charging_led") then Device:toggleChargingLED(false) @@ -1508,8 +1672,11 @@ function UIManager:_afterNotCharging() self:broadcastEvent(Event:new("NotCharging")) end --- Executes all the operations of a suspending request. This function usually puts the device into --- suspension. +--[[-- +Executes all the operations of a suspension (i.e., sleep) request. + +This function usually puts the device into suspension. +]] function UIManager:suspend() if Device:isCervantes() or Device:isKobo() or Device:isSDL() or Device:isRemarkable() or Device:isSonyPRSTUX() then self.event_handlers["Suspend"]() @@ -1520,7 +1687,11 @@ function UIManager:suspend() end end --- Executes all the operations of a resume request. This function usually wakes up the device. +--[[-- +Executes all the operations of a resume (i.e., wakeup) request. + +This function usually wakes up the device. +]] function UIManager:resume() if Device:isCervantes() or Device:isKobo() or Device:isSDL() or Device:isRemarkable() or Device:isSonyPRSTUX() then self.event_handlers["Resume"]() @@ -1529,19 +1700,27 @@ function UIManager:resume() end end --- Release standby lock once. We're done with whatever we were doing in the background. --- Standby is re-enabled only after all issued prevents are paired with allowStandby for each one. +--[[-- +Release standby lock. + +Called once we're done with whatever we were doing in the background. +Standby is re-enabled only after all issued prevents are paired with allowStandby for each one. +]] function UIManager:allowStandby() assert(self._prevent_standby_count > 0, "allowing standby that isn't prevented; you have an allow/prevent mismatch somewhere") self._prevent_standby_count = self._prevent_standby_count - 1 end --- Prevent standby, ie something is happening in background, yet UI may tick. +--[[-- +Prevent standby. + +i.e., something is happening in background, yet UI may tick. +]] function UIManager:preventStandby() self._prevent_standby_count = self._prevent_standby_count + 1 end --- Allow/prevent calls above can interminently allow standbys, but we're not interested until +-- The allow/prevent calls above can interminently allow standbys, but we're not interested until -- the state change crosses UI tick boundary, which is what self._prev_prevent_standby_count is tracking. function UIManager:_standbyTransition() if self._prevent_standby_count == 0 and self._prev_prevent_standby_count > 0 then @@ -1558,10 +1737,12 @@ function UIManager:_standbyTransition() self._prev_prevent_standby_count = self._prevent_standby_count end +--- Broadcasts a `FlushSettings` Event to *all* widgets. function UIManager:flushSettings() self:broadcastEvent(Event:new("FlushSettings")) end +--- Sanely restart KOReader (on supported platforms). function UIManager:restartKOReader() self:quit() -- This is just a magic number to indicate the restart request for shell scripts. diff --git a/frontend/ui/widget/button.lua b/frontend/ui/widget/button.lua index ed03628c6..413eb7a21 100644 --- a/frontend/ui/widget/button.lua +++ b/frontend/ui/widget/button.lua @@ -180,11 +180,10 @@ function Button:enable() if not self.enabled then if self.text then self.label_widget.fgcolor = Blitbuffer.COLOR_BLACK - self.enabled = true else self.label_widget.dim = false - self.enabled = true end + self.enabled = true end end @@ -192,11 +191,10 @@ function Button:disable() if self.enabled then if self.text then self.label_widget.fgcolor = Blitbuffer.COLOR_DARK_GRAY - self.enabled = false else self.label_widget.dim = true - self.enabled = false end + self.enabled = false end end @@ -233,135 +231,112 @@ function Button:showHide(show) end end -function Button:onTapSelectButton() - -- NOTE: We have a few tricks up our sleeve in case our parent is inside a translucent MovableContainer... - local was_translucent = self.show_parent and self.show_parent.movable and self.show_parent.movable.alpha - -- We make a distinction between transparency pre- and post- callback, because if a widget *was* transparent, - -- but no longer is post-callback, we want to ensure that we refresh the *full* container, - -- instead of just the button's frame, in order to avoid leaving bits of the widget transparent ;). - local is_translucent = was_translucent +-- Used by onTapSelectButton to handle visual feedback when flash_ui is enabled +function Button:_doFeedbackHighlight() + -- NOTE: self[1] -> self.frame, if you're confused about what this does vs. onFocus/onUnfocus ;). + if self.text then + -- We only want the button's *highlight* to have rounded corners (otherwise they're redundant, same color as the bg). + -- The nil check is to discriminate the default from callers that explicitly request a specific radius. + if self[1].radius == nil then + self[1].radius = Size.radius.button + -- And here, it's easier to just invert the bg/fg colors ourselves, + -- so as to preserve the rounded corners in one step. + self[1].background = self[1].background:invert() + self.label_widget.fgcolor = self.label_widget.fgcolor:invert() + -- We do *NOT* set the invert flag, because it just adds an invertRect step at the end of the paintTo process, + -- and we've already taken care of inversion in a way that won't mangle the rounded corners. + else + self[1].invert = true + end + + -- This repaints *now*, unlike setDirty + UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y) + else + self[1].invert = true + UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) + end + UIManager:setDirty(nil, "fast", self[1].dimen) +end + +function Button:_undoFeedbackHighlight(is_translucent) + if self.text then + if self[1].radius == Size.radius.button then + self[1].radius = nil + self[1].background = self[1].background:invert() + self.label_widget.fgcolor = self.label_widget.fgcolor:invert() + else + self[1].invert = false + end + UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y) + else + self[1].invert = false + UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) + end + + if is_translucent then + -- If our parent belongs to a translucent MovableContainer, we need to repaint it on unhighlight in order to honor alpha, + -- because our highlight/unhighlight will have made the Button fully opaque. + -- UIManager will detect transparency and then takes care of also repainting what's underneath us to avoid alpha layering glitches. + UIManager:setDirty(self.show_parent, "ui", self[1].dimen) + else + -- In case the callback itself won't enqueue a refresh region that includes us, do it ourselves. + -- If the button is disabled, switch to UI to make sure the gray comes through unharmed ;). + UIManager:setDirty(nil, self.enabled and "fast" or "ui", self[1].dimen) + end +end +function Button:onTapSelectButton() if self.enabled and self.callback then if G_reader_settings:isFalse("flash_ui") then self.callback() else - -- We need to keep track of whether we actually flipped the frame's invert flag ourselves, - -- to handle the rounded corners shenanigan in the post-callback invert check... - local inverted = false - -- NOTE: self[1] -> self.frame, if you're confused about what this does vs. onFocus/onUnfocus ;). - if self.text then - -- We only want the button's *highlight* to have rounded corners (otherwise they're redundant, same color as the bg). - -- The nil check is to discriminate the default from callers that explicitly request a specific radius. - if self[1].radius == nil then - self[1].radius = Size.radius.button - -- And here, it's easier to just invert the bg/fg colors ourselves, - -- so as to preserve the rounded corners in one step. - self[1].background = self[1].background:invert() - self.label_widget.fgcolor = self.label_widget.fgcolor:invert() - -- We do *NOT* set the invert flag, because it just adds an invertRect step at the end of the paintTo process, - -- and we've already taken care of inversion in a way that won't mangle the rounded corners. - -- The "inverted" local flag allows us to fudge the "did callback invert the frame?" check for these buttons, - -- otherwise setting the invert flag here breaks the highlight for vsync buttons... - else - self[1].invert = true - inverted = true - end - - UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y) - else - self[1].invert = true - inverted = true - UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) + -- NOTE: We have a few tricks up our sleeve in case our parent is inside a translucent MovableContainer... + local is_translucent = self.show_parent and self.show_parent.movable and self.show_parent.movable.alpha + + -- Highlight + -- + self:_doFeedbackHighlight() + + -- Force the refresh by draining the refresh queue *now*, so we have a chance to see the highlight on its own, before whatever the callback will do. + if not self.vsync then + -- NOTE: Except when a Button is flagged vsync, in which case we *want* to bundle the highlight with the callback, to prevent further delays + UIManager:forceRePaint() end - UIManager:setDirty(nil, function() - return "fast", self[1].dimen - end) - -- Force the repaint *now*, so we don't have to delay the callback to see the highlight... + -- Unhighlight + -- + -- We'll *paint* the unhighlight now, because at this point we can still be sure that our widget exists, + -- and that anything we do will not impact whatever the callback does (i.e., that we draw *below* whatever the callback might show). + -- We won't *fence* the refresh (i.e., it's queued, but we don't actually drain the queue yet), though, to ensure that we do not delay the callback, and that the unhighlight essentially blends into whatever the callback does. + -- Worst case scenario, we'll simply have "wasted" a tiny subwidget repaint if the callback closed us, + -- but doing it this way allows us to avoid a large array of potential interactions with whatever the callback may paint/refresh if we were to handle the unhighlight post-callback, + -- which would require a number of possibly brittle heuristics to handle. + -- NOTE: If a Button is marked vsync, we want to keep it highlighted for now (in order for said highlight to be visible during the callback refresh), we'll remove the highlight post-callback. if not self.vsync then - -- NOTE: Allow bundling the highlight with the callback when we request vsync, to prevent further delays - UIManager:forceRePaint() -- Ensures we have a chance to see the highlight + self:_undoFeedbackHighlight(is_translucent) end + + -- Callback + -- self.callback() + -- Check if the callback reset transparency... - is_translucent = was_translucent and self.show_parent.movable.alpha - -- We don't want to fence the callback when we're *still* translucent, because we want a *single* refresh post-callback *and* post-unhighlight, - -- in order to avoid flickering. - if not is_translucent then - UIManager:forceRePaint() -- Ensures whatever the callback wanted to paint will be shown *now*... - end + is_translucent = is_translucent and self.show_parent.movable.alpha + + UIManager:forceRePaint() -- Ensures whatever the callback wanted to paint will be shown *now*... if self.vsync then - -- NOTE: This is mainly useful when the callback caused a REAGL update that we do not explicitly fence already, - -- (i.e., Kobo Mk. 7). + -- NOTE: This is mainly useful when the callback caused a REAGL update that we do not explicitly fence via MXCFB_WAIT_FOR_UPDATE_COMPLETE already, (i.e., Kobo Mk. 7). UIManager:waitForVSync() -- ...and that the EPDC will not wait to coalesce it with the *next* update, - -- because that would have a chance to noticeably delay it until the unhighlight. - end - - if not self[1] or (inverted and not self[1].invert) or not self[1].dimen then - -- If the frame widget no longer exists (destroyed, re-init'ed by setText(), or is no longer inverted: we have nothing to invert back - -- NOTE: This cannot catch orphaned Button instances, c.f., the isSubwidgetShown(self) check below for that. - return true + -- because that would have a chance to noticeably delay it until the unhighlight. end - -- Reset colors early, regardless of what we do later, to avoid code duplication - self[1].invert = false - if self.text then - if self[1].radius == Size.radius.button then - self[1].radius = nil - self[1].background = self[1].background:invert() - self.label_widget.fgcolor = self.label_widget.fgcolor:invert() - end - end - - -- If the callback closed our parent (which may not always be the top-level widget, or even *a* window-level widget), we're done - local top_widget = UIManager:getTopWidget() - -- When VirtualKeyboard is involved, it steals the top widget slot... So, instead, look below it to find the *effective* top-level widget, because we generally don't give a damn about VK here... - if top_widget == "VirtualKeyboard" then - top_widget = UIManager:getSecondTopmostWidget() - end - if top_widget == self.show_parent or UIManager:isSubwidgetShown(self.show_parent) then - -- If the button can no longer be found inside a shown widget, abort early - -- (this allows us to catch widgets that instanciate *new* Buttons on every update... (e.g., some ButtonTable users) :() - if not UIManager:isSubwidgetShown(self) then - return true - end - - -- If our parent is no longer the toplevel widget... - if top_widget ~= self.show_parent then - -- ... and the new toplevel covers the full screen, we're done. - if top_widget.covers_fullscreen then - return true - end - - -- ... and toplevel is now a true modal, and our highlight would clash with that modal's region, - -- we have no other choice than repainting the full stack... - if top_widget.modal and self[1].dimen:intersectWith(UIManager:getPreviousRefreshRegion()) then - -- Much like in TouchMenu, the fact that the two intersect means we have no choice but to repaint the full stack to avoid half-painted widgets... - UIManager:waitForVSync() - UIManager:setDirty(self.show_parent, function() - return "ui", self[1].dimen - end) - - -- It's a sane exit, handle the return the same way. - if self.readonly ~= true then - return true - end - end - end - - if self.text then - UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y) - else - UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) - end - -- If the button was disabled, switch to UI to make sure the gray comes through unharmed ;). - UIManager:setDirty(nil, function() - return self.enabled and "fast" or "ui", self[1].dimen - end) - --UIManager:forceRePaint() -- Ensures the unhighlight happens now, instead of potentially waiting and having it batched with something else. - else - -- Callback closed our parent, we're done - return true + -- Unhighlight + -- + -- NOTE: If a Button is marked vsync, we have a guarantee from the programmer that the widget it belongs to is still alive and top-level post-callback, + -- so we can do this safely without risking UI glitches. + if self.vsync then + self:_undoFeedbackHighlight(is_translucent) + UIManager:forceRePaint() end end elseif self.tap_input then @@ -370,15 +345,6 @@ function Button:onTapSelectButton() self:onInput(self.tap_input_func()) end - -- If our parent belongs to a translucent MovableContainer, repaint all the things to honor alpha without layering glitches, - -- and refresh the full container, because the widget might have inhibited its own setDirty call to avoid flickering (c.f., *SpinWidget). - if was_translucent then - -- If the callback reset the transparency, we only need to repaint our parent - UIManager:setDirty(is_translucent and "all" or self.show_parent, function() - return "ui", self.show_parent.movable.dimen - end) - end - if self.readonly ~= true then return true end @@ -395,6 +361,7 @@ function Button:refresh() return end UIManager:widgetRepaint(self[1], self[1].dimen.x, self.dimen.y) + UIManager:setDirty(nil, function() return self.enabled and "fast" or "ui", self[1].dimen end) diff --git a/frontend/ui/widget/checkbutton.lua b/frontend/ui/widget/checkbutton.lua index a9ec0fbd2..18d386095 100644 --- a/frontend/ui/widget/checkbutton.lua +++ b/frontend/ui/widget/checkbutton.lua @@ -32,6 +32,7 @@ local CheckButton = InputContainer:new{ checked = false, enabled = true, face = Font:getFace("cfont"), + background = Blitbuffer.COLOR_WHITE, overlap_align = "right", text = nil, toggle_text = nil, @@ -67,6 +68,7 @@ function CheckButton:initCheckButton(checked) } self._frame = FrameContainer:new{ bordersize = 0, + background = self.background, margin = 0, padding = 0, self._horizontalgroup, @@ -99,24 +101,32 @@ function CheckButton:onTapCheckButton() if G_reader_settings:isFalse("flash_ui") then self.callback() else + -- c.f., ui/widget/iconbutton for the canonical documentation about the flash_ui code flow + + -- Unlike RadioButton, the frame's width stops at the text width, but we want our highlight to span the full width... + -- (That's when we have one, some callers don't pass a width, so, handle that, too). + local highlight_dimen = self.dimen + highlight_dimen.w = self.width and self.width or self.dimen.w + + -- Highlight + -- self[1].invert = true - UIManager:widgetRepaint(self[1], self.dimen.x, self.dimen.y) - UIManager:setDirty(nil, function() - return "fast", self.dimen - end) + UIManager:widgetInvert(self[1], highlight_dimen.x, highlight_dimen.y, highlight_dimen.w) + UIManager:setDirty(nil, "fast", highlight_dimen) - -- Force the repaint *now*, so we don't have to delay the callback to see the invert... UIManager:forceRePaint() - self.callback() - --UIManager:forceRePaint() -- Unnecessary, the check/uncheck process involves too many repaints already - --UIManager:waitForVSync() + -- Unhighlight + -- self[1].invert = false - UIManager:widgetRepaint(self[1], self.dimen.x, self.dimen.y) - UIManager:setDirty(nil, function() - return "fast", self.dimen - end) - --UIManager:forceRePaint() + UIManager:widgetInvert(self[1], highlight_dimen.x, highlight_dimen.y, highlight_dimen.w) + UIManager:setDirty(nil, "ui", highlight_dimen) + + -- Callback + -- + self.callback() + + UIManager:forceRePaint() end elseif self.tap_input then self:onInput(self.tap_input) @@ -140,14 +150,14 @@ end function CheckButton:check() self:initCheckButton(true) UIManager:setDirty(self.parent, function() - return "fast", self.dimen + return "ui", self.dimen end) end function CheckButton:unCheck() self:initCheckButton(false) UIManager:setDirty(self.parent, function() - return "fast", self.dimen + return "ui", self.dimen end) end @@ -164,7 +174,6 @@ function CheckButton:disable() self:initCheckButton(false) UIManager:setDirty(self.parent, function() return "ui", self.dimen - -- best to use "ui" instead of "fast" when we make things gray end) end diff --git a/frontend/ui/widget/container/alphacontainer.lua b/frontend/ui/widget/container/alphacontainer.lua index f0e56457d..0d51c9284 100644 --- a/frontend/ui/widget/container/alphacontainer.lua +++ b/frontend/ui/widget/container/alphacontainer.lua @@ -1,6 +1,5 @@ --[[-- -AlphaContainer will paint its content (1 widget) onto lower levels using -a transparency (0..1) +AlphaContainer will paint its content (a single widget) at the specified opacity level (0..1) Example: @@ -24,65 +23,29 @@ local AlphaContainer = WidgetContainer:new{ alpha = 1, -- we cache a blitbuffer object for re-use here: private_bb = nil, - -- we save the underlying area here: - background_bb = nil, - background_bb_x = nil, - background_bb_y = nil } function AlphaContainer:paintTo(bb, x, y) local contentSize = self[1]:getSize() - local private_bb = self.private_bb - if self.background_bb then - -- NOTE: Best as I can tell, this was an attempt at avoiding alpha layering issues when an AlphaContainer is repainted *at the same coordinates* AND *over the same background*. - -- Unfortunately, those are hard constraints to respect, and, while we can take care of the first by invalidating the cache if coordinates have changed, - -- we can't do anything about the second (and that's exactly what happens in ReaderUI when paging around, for example: that'll obviously have changed what's below AlphaContainer ;)). - -- FWIW, MovableContainer's alpha handling rely on callers using setDirty("all") to force a repaint of the whole stack to avoid layering issues. - -- A better approach would probably involve letting UIManager handle it: if it finds a dirty translucent widget, mark all the widgets below it dirty, too... - if self.background_bb_x == x and self.background_bb_y == y then - bb:blitFrom(self.background_bb, self.background_bb_x, self.background_bb_y) - else - -- We moved, invalidate the bg cache. - self.background_bb:free() - self.background_bb = nil - self.background_bb_x = nil - self.background_bb_y = nil - end - end - - if not private_bb - or private_bb:getWidth() ~= contentSize.w - or private_bb:getHeight() ~= contentSize.h + if not self.private_bb + or self.private_bb:getWidth() ~= contentSize.w + or self.private_bb:getHeight() ~= contentSize.h then - if private_bb then - private_bb:free() -- free the one we're going to replace + if self.private_bb then + self.private_bb:free() -- free the one we're going to replace end - -- create private blitbuffer for our child widget to paint to - private_bb = Blitbuffer.new(contentSize.w, contentSize.h, bb:getType()) - self.private_bb = private_bb - - -- save what is below our painting area - if not self.background_bb - or self.background_bb:getWidth() ~= contentSize.w - or self.background_bb:getHeight() ~= contentSize.h - then - if self.background_bb then - self.background_bb:free() -- free the one we're going to replace - end - self.background_bb = Blitbuffer.new(contentSize.w, contentSize.h, bb:getType()) - end - self.background_bb:blitFrom(bb, 0, 0, x, y) - self.background_bb_x = x - self.background_bb_y = y + -- create a private blitbuffer for our child widget to paint to + self.private_bb = Blitbuffer.new(contentSize.w, contentSize.h, bb:getType()) + -- fill it with our usual background color + self.private_bb:fill(Blitbuffer.COLOR_WHITE) end - -- now have our child widget paint to the private blitbuffer - private_bb:fill(Blitbuffer.COLOR_WHITE) - self[1]:paintTo(private_bb, 0, 0) + -- now, compose our child widget's content on our private blitbuffer canvas + self[1]:paintTo(self.private_bb, 0, 0) - -- blit the private blitbuffer to our parent blitbuffer - bb:addblitFrom(private_bb, x, y, nil, nil, nil, nil, self.alpha) + -- and finally blit the private blitbuffer to the target blitbuffer at the requested opacity level + bb:addblitFrom(self.private_bb, x, y, 0, 0, contentSize.w, contentSize.h, self.alpha) end function AlphaContainer:onCloseWidget() @@ -90,10 +53,6 @@ function AlphaContainer:onCloseWidget() self.private_bb:free() self.private_bb = nil end - if self.background_bb then - self.background_bb:free() - self.background_bb = nil - end end diff --git a/frontend/ui/widget/container/movablecontainer.lua b/frontend/ui/widget/container/movablecontainer.lua index 12f0c63fd..383584526 100644 --- a/frontend/ui/widget/container/movablecontainer.lua +++ b/frontend/ui/widget/container/movablecontainer.lua @@ -46,6 +46,9 @@ local MovableContainer = InputContainer:new{ -- Original painting position from outer widget _orig_x = nil, _orig_y = nil, + + -- We cache a compose canvas for alpha handling + compose_bb = nil, } function MovableContainer:init() @@ -113,20 +116,42 @@ function MovableContainer:paintTo(bb, x, y) self.dimen.y = y + self._moved_offset_y if self.alpha then - -- Create private blitbuffer for our child widget to paint to - local private_bb = Blitbuffer.new(bb:getWidth(), bb:getHeight(), bb:getType()) - private_bb:fill(Blitbuffer.COLOR_WHITE) -- for round corners' outside to not stay black - self[1]:paintTo(private_bb, self.dimen.x, self.dimen.y) - -- And blend our private blitbuffer over the original bb - bb:addblitFrom(private_bb, self.dimen.x, self.dimen.y, self.dimen.x, self.dimen.y, - self.dimen.w, self.dimen.h, self.alpha) - private_bb:free() + -- Create/Recreate the compose cache if we changed screen geometry + if not self.compose_bb + or self.compose_bb:getWidth() ~= bb:getWidth() + or self.compose_bb:getHeight() ~= bb:getHeight() + then + if self.compose_bb then + self.compose_bb:free() + end + -- create a canvas for our child widget to paint to + self.compose_bb = Blitbuffer.new(bb:getWidth(), bb:getHeight(), bb:getType()) + -- fill it with our usual background color + self.compose_bb:fill(Blitbuffer.COLOR_WHITE) + end + + -- now, compose our child widget's content on our canvas + -- NOTE: Unlike AlphaContainer, we aim to support interactive widgets. + -- Most InputContainer-based widgets register their touchzones at paintTo time, + -- and they rely on the target coordinates fed to paintTo for proper on-screen positioning. + -- As such, we have to compose on a target bb sized canvas, at the expected coordinates. + self[1]:paintTo(self.compose_bb, self.dimen.x, self.dimen.y) + + -- and finally blit the canvas to the target blitbuffer at the requested opacity level + bb:addblitFrom(self.compose_bb, self.dimen.x, self.dimen.y, self.dimen.x, self.dimen.y, self.dimen.w, self.dimen.h, self.alpha) else -- No alpha, just paint self[1]:paintTo(bb, self.dimen.x, self.dimen.y) end end +function MovableContainer:onCloseWidget() + if self.compose_bb then + self.compose_bb:free() + self.compose_bb = nil + end +end + function MovableContainer:_moveBy(dx, dy, restrict_to_screen) logger.dbg("MovableContainer:_moveBy:", dx, dy) if dx and dy then diff --git a/frontend/ui/widget/dictquicklookup.lua b/frontend/ui/widget/dictquicklookup.lua index 8e21e26a3..1c1961945 100644 --- a/frontend/ui/widget/dictquicklookup.lua +++ b/frontend/ui/widget/dictquicklookup.lua @@ -572,6 +572,7 @@ function DictQuickLookup:init() face = self.content_face, width = self.content_width, height = self.definition_height, + for_measurement_only = true, -- flag it as a dummy, so it won't trigger any bogus repaint/refresh... } self.definition_line_height = test_widget:getLineHeight() test_widget:free() @@ -835,12 +836,11 @@ function DictQuickLookup:update() -- If we're translucent, reset alpha to make the new definition actually readable. if self.movable.alpha then self.movable.alpha = nil - -- And skip the setDirty, Button will handle it post-callback & post-unhighlight. - else - UIManager:setDirty(self, function() - return "partial", self.dict_frame.dimen - end) end + + UIManager:setDirty(self, function() + return "partial", self.dict_frame.dimen + end) end function DictQuickLookup:getInitialVisibleArea() @@ -1264,9 +1264,7 @@ function DictQuickLookup:lookupWikipedia(get_fullpage) is_sane = false end self:resyncWikiLanguages() - -- (With Event, we need to pass false instead of nil if word_box is nil, - -- otherwise next arguments are discarded) - self.ui:handleEvent(Event:new("LookupWikipedia", word, is_sane, self.word_box and self.word_box or false, get_fullpage)) + self.ui:handleEvent(Event:new("LookupWikipedia", word, is_sane, self.word_box, get_fullpage)) end return DictQuickLookup diff --git a/frontend/ui/widget/doublespinwidget.lua b/frontend/ui/widget/doublespinwidget.lua index 4f2c0f1f0..e24e71e30 100644 --- a/frontend/ui/widget/doublespinwidget.lua +++ b/frontend/ui/widget/doublespinwidget.lua @@ -281,12 +281,9 @@ function DoubleSpinWidget:update() }, self.movable, } - -- If we're translucent, Button itself will handle that post-callback, in order to preserve alpha without flickering. - if not self.movable.alpha then - UIManager:setDirty(self, function() - return "ui", self.widget_frame.dimen - end) - end + UIManager:setDirty(self, function() + return "ui", self.widget_frame.dimen + end) end function DoubleSpinWidget:hasMoved() diff --git a/frontend/ui/widget/eventlistener.lua b/frontend/ui/widget/eventlistener.lua index 0fcbec4fc..2f7fc0d30 100644 --- a/frontend/ui/widget/eventlistener.lua +++ b/frontend/ui/widget/eventlistener.lua @@ -1,6 +1,6 @@ --[[-- The EventListener is an interface that handles events. This is the base class -for @{ui.widget.widget} +for @{ui.widget.widget|Widget} EventListeners have a rudimentary event handler/dispatcher that will call a method "onEventName" for an event with name @@ -29,7 +29,7 @@ By default, it's `"on"..Event.name`. function EventListener:handleEvent(event) if self[event.handler] then --print("EventListener:handleEvent:", event.handler, "handled by", debug.getinfo(self[event.handler], "S").short_src, self) - return self[event.handler](self, unpack(event.args)) + return self[event.handler](self, unpack(event.args, 1, event.argc)) end end diff --git a/frontend/ui/widget/iconbutton.lua b/frontend/ui/widget/iconbutton.lua index b766c9521..ce12cde66 100644 --- a/frontend/ui/widget/iconbutton.lua +++ b/frontend/ui/widget/iconbutton.lua @@ -95,55 +95,38 @@ function IconButton:onTapIconButton() if G_reader_settings:isFalse("flash_ui") then self.callback() else + -- c.f., ui/widget/button for more gnarly details about the implementation, but the flow of the flash_ui codepath essentially goes like this: + -- 1. Paint the highlight + -- 2. Refresh the highlighted item (so we can see the highlight) + -- 3. Paint the unhighlight + -- 4. Do NOT refresh the highlighted item, but enqueue a refresh request + -- 5. Run the callback + -- 6. Explicitly drain the paint & refresh queues; i.e., refresh (so we get to see both the callback results, and the unhighlight). + + -- Highlight + -- self.image.invert = true - -- For ConfigDialog icons, we can't avoid that initial repaint... UIManager:widgetInvert(self.image, self.dimen.x + self.padding_left, self.dimen.y + self.padding_top) - UIManager:setDirty(nil, function() - return "fast", self.dimen - end) + UIManager:setDirty(nil, "fast", self.dimen) - -- Force the repaint *now*, so we don't have to delay the callback to see the invert... UIManager:forceRePaint() - self.callback() - UIManager:forceRePaint() - --UIManager:waitForVSync() + -- Unhighlight + -- self.image.invert = false - -- If the callback closed our parent (which may not always be the top-level widget, or even *a* window-level widget), we're done - local top_widget = UIManager:getTopWidget() - if top_widget == self.show_parent or UIManager:isSubwidgetShown(self.show_parent) then - -- If the callback popped up the VK, it prevents us from finessing this any further, - -- because getPreviousRefreshRegion will return the VK's region, - -- and it's impossible to get the actual geometry of *only* the InputText of an InputDialog, - -- making the same kind of getSecondTopmostWidget trickery as in Button useless, - -- so repaint the whole stack instead. - if top_widget == "VirtualKeyboard" then - UIManager:waitForVSync() - UIManager:setDirty(self.show_parent, function() - return "ui", self.dimen - end) - return true - end - - -- If the callback popped up a modal above us, repaint the whole stack - if top_widget ~= self.show_parent and top_widget.modal and self.dimen:intersectWith(UIManager:getPreviousRefreshRegion()) then - UIManager:waitForVSync() - UIManager:setDirty(self.show_parent, function() - return "ui", self.dimen - end) - return true - end - - -- Otherwise, we can unhighlight it safely - UIManager:widgetInvert(self.image, self.dimen.x + self.padding_left, self.dimen.y + self.padding_top) - UIManager:setDirty(nil, function() - return "fast", self.dimen - end) - else - -- Callback closed our parent, we're done - return true - end - --UIManager:forceRePaint() + UIManager:widgetInvert(self.image, self.dimen.x + self.padding_left, self.dimen.y + self.padding_top) + + -- Callback + -- + self.callback() + + -- NOTE: plugins/coverbrowser.koplugin/covermenu (ab)uses UIManager:clearRenderStack, + -- so we need to enqueue the actual refresh request for the unhighlight post-callback, + -- otherwise, it's lost. + -- This changes nothing in practice, since we follow by explicitly requesting to drain the refresh queue ;). + UIManager:setDirty(nil, "fast", self.dimen) + + UIManager:forceRePaint() end return true end diff --git a/frontend/ui/widget/inputdialog.lua b/frontend/ui/widget/inputdialog.lua index 12d8dec3f..895e44065 100644 --- a/frontend/ui/widget/inputdialog.lua +++ b/frontend/ui/widget/inputdialog.lua @@ -319,6 +319,7 @@ function InputDialog:init() lang = self.lang, -- these might influence height para_direction_rtl = self.para_direction_rtl, auto_para_direction = self.auto_para_direction, + for_measurement_only = true, -- flag it as a dummy, so it won't trigger any bogus repaint/refresh... } local text_height = input_widget:getTextHeight() local line_height = input_widget:getLineHeight() diff --git a/frontend/ui/widget/inputtext.lua b/frontend/ui/widget/inputtext.lua index d9cf41282..c2d46ea38 100644 --- a/frontend/ui/widget/inputtext.lua +++ b/frontend/ui/widget/inputtext.lua @@ -58,6 +58,7 @@ local InputText = InputContainer:new{ is_password_type = false, -- set to true if original text_type == "password" is_text_editable = true, -- whether text is utf8 reversible and editing won't mess content is_text_edited = false, -- whether text has been updated + for_measurement_only = nil, -- When the widget is a one-off used to compute text height } -- only use PhysicalKeyboard if the device does not have touch screen @@ -318,6 +319,7 @@ function InputText:initTextBox(text, char_added) lang = self.lang, -- these might influence height para_direction_rtl = self.para_direction_rtl, auto_para_direction = self.auto_para_direction, + for_measurement_only = true, -- flag it as a dummy, so it won't trigger any bogus repaint/refresh... } self.height = text_widget:getTextHeight() self.scroll = true @@ -343,6 +345,7 @@ function InputText:initTextBox(text, char_added) dialog = self.parent, scroll_callback = self.scroll_callback, scroll_by_pan = self.scroll_by_pan, + for_measurement_only = self.for_measurement_only, } else self.text_widget = TextBoxWidget:new{ @@ -362,6 +365,7 @@ function InputText:initTextBox(text, char_added) width = self.width, height = self.height, dialog = self.parent, + for_measurement_only = self.for_measurement_only, } end -- Get back possibly modified charpos and virtual_line_num @@ -388,9 +392,12 @@ function InputText:initTextBox(text, char_added) self[1] = self._frame self.dimen = self._frame:getSize() --- @fixme self.parent is not always in the widget stack (BookStatusWidget) - UIManager:setDirty(self.parent, function() - return "ui", self.dimen - end) + -- Don't even try to refresh dummy widgets used for text height computations... + if not self.for_measurement_only then + UIManager:setDirty(self.parent, function() + return "ui", self.dimen + end) + end if self.edit_callback then self.edit_callback(self.is_text_edited) end diff --git a/frontend/ui/widget/keyvaluepage.lua b/frontend/ui/widget/keyvaluepage.lua index 4a8d0a511..556a81801 100644 --- a/frontend/ui/widget/keyvaluepage.lua +++ b/frontend/ui/widget/keyvaluepage.lua @@ -145,8 +145,8 @@ function KeyValueItem:init() local available_width = frame_internal_width - middle_padding -- Default widths (and position of value widget) if each text fits in 1/2 screen width - local key_w = frame_internal_width / 2 - middle_padding - local value_w = frame_internal_width / 2 + local key_w = math.floor(frame_internal_width / 2 - middle_padding) + local value_w = math.floor(frame_internal_width / 2) local key_widget = TextWidget:new{ text = self.key, @@ -281,45 +281,27 @@ function KeyValueItem:onTap() if G_reader_settings:isFalse("flash_ui") then self.callback() else + -- c.f., ui/widget/iconbutton for the canonical documentation about the flash_ui code flow + + -- Highlight + -- self[1].invert = true UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) - UIManager:setDirty(nil, function() - return "fast", self[1].dimen - end) + UIManager:setDirty(nil, "fast", self[1].dimen) - -- Force the repaint *now*, so we don't have to delay the callback to see the invert... UIManager:forceRePaint() + + -- Unhighlight + -- + self[1].invert = false + UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) + UIManager:setDirty(nil, "ui", self[1].dimen) + + -- Callback + -- self.callback() + UIManager:forceRePaint() - --UIManager:waitForVSync() - - -- Has to be scheduled *after* the dict delays for the lookup history pages... - UIManager:scheduleIn(0.75, function() - self[1].invert = false - -- If we've ended up below something, things get trickier. - local top_widget = UIManager:getTopWidget() - if top_widget ~= self.show_parent then - -- It's generally tricky to get accurate dimensions out of whatever was painted above us, - -- so cheat by comparing against the previous refresh region... - if self[1].dimen:intersectWith(UIManager:getPreviousRefreshRegion()) then - -- If that something is a modal (e.g., dictionary D/L), repaint the whole stack - if top_widget.modal then - UIManager:setDirty(self.show_parent, function() - return "ui", self[1].dimen - end) - return true - else - -- Otherwise, skip the repaint - return true - end - end - end - UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) - UIManager:setDirty(nil, function() - return "ui", self[1].dimen - end) - --UIManager:forceRePaint() - end) end end return true @@ -484,7 +466,7 @@ function KeyValuePage:init() kv_page = self, } -- setup main content - self.item_margin = self.item_height / 4 + self.item_margin = math.floor(self.item_height / 4) local line_height = self.item_height + 2 * self.item_margin local content_height = self.dimen.h - self.title_bar:getSize().h - self.page_info:getSize().h self.items_per_page = math.floor(content_height / line_height) diff --git a/frontend/ui/widget/menu.lua b/frontend/ui/widget/menu.lua index 7d9a4faa3..03d259285 100644 --- a/frontend/ui/widget/menu.lua +++ b/frontend/ui/widget/menu.lua @@ -471,76 +471,31 @@ function MenuItem:onTapSelect(arg, ges) end) coroutine.resume(co) else + -- c.f., ui/widget/iconbutton for the canonical documentation about the flash_ui code flow + + -- Highlight + -- self[1].invert = true UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) - UIManager:setDirty(nil, function() - return "fast", self[1].dimen - end) + UIManager:setDirty(nil, "fast", self[1].dimen) - -- Force the repaint *now*, so we don't have to delay the callback to see the invert... UIManager:forceRePaint() + + -- Unhighlight + -- + self[1].invert = false + UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) + UIManager:setDirty(nil, "ui", self[1].dimen) + + -- Callback + -- logger.dbg("creating coroutine for menu select") local co = coroutine.create(function() self.menu:onMenuSelect(self.table, pos) end) coroutine.resume(co) - UIManager:forceRePaint() - --UIManager:waitForVSync() - - self[1].invert = false - - -- Most Menu entries will actually update the full menu, but they may also pop up a few various things, - -- so, pilfer a few heuristics from TouchMenu... - local top_widget = UIManager:getTopWidget() - - -- If we're still on top, we're done, as the full list of items has probably been updated by the callback - if top_widget == self.show_parent then - -- Unless the callback actually did nothing (e.g., PathChooser in Classic view) - if UIManager:getPreviousRefreshRegion() == self[1].dimen then - -- The highlight matches the last refresh, assume this means that the callback did nothing, so just unhighlight... - UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) - UIManager:setDirty(nil, function() - return "ui", self[1].dimen - end) - end - -- Otherwise, we assume the callback effectively updated & repainted the list of items. - -- Both cases warrant an early return. - return true - else - -- If the callback opened a *different* full-screen widget, we're done - if top_widget.covers_fullscreen then - return true - end - end - -- If the callback opened the Virtual Keyboard, it gets trickier - if top_widget == "VirtualKeyboard" then - -- Unfortunately, we can't really tell full-screen widgets apart from - -- stuff that might just pop the keyboard for an InputText box... - -- So, a full fenced redraw it is... - UIManager:waitForVSync() - UIManager:setDirty(self.show_parent, function() - return "ui", self[1].dimen - end) - return true - end - - -- If a modal was opened outside of our highlight region, we can unhighlight safely - if self[1].dimen:notIntersectWith(UIManager:getPreviousRefreshRegion()) then - UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) - UIManager:setDirty(nil, function() - return "ui", self[1].dimen - end) - else - -- That leaves modals that might have been displayed on top of the highlighted menu entry, in which case, - -- we can't take any shortcuts, as it would invert/paint *over* the popop. - -- Instead, fence the callback to avoid races, and repaint the *full* widget stack properly. - UIManager:waitForVSync() - UIManager:setDirty(self.show_parent, function() - return "ui", self[1].dimen - end) - end - --UIManager:forceRePaint() + UIManager:forceRePaint() end return true end @@ -550,39 +505,27 @@ function MenuItem:onHoldSelect(arg, ges) if G_reader_settings:isFalse("flash_ui") then self.menu:onMenuHold(self.table, pos) else + -- c.f., ui/widget/iconbutton for the canonical documentation about the flash_ui code flow + + -- Highlight + -- self[1].invert = true UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) - UIManager:setDirty(nil, function() - return "fast", self[1].dimen - end) + UIManager:setDirty(nil, "fast", self[1].dimen) - -- Force the repaint *now*, so we don't have to delay the callback to see the invert... UIManager:forceRePaint() - self.menu:onMenuHold(self.table, pos) - UIManager:forceRePaint() - --UIManager:waitForVSync() + -- Unhighlight + -- self[1].invert = false + UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) + UIManager:setDirty(nil, "ui", self[1].dimen) - -- Same idea as for tap, minus the various things that make no sense for a hold callback... - local top_widget = UIManager:getTopWidget() + -- Callback + -- + self.menu:onMenuHold(self.table, pos) - -- If we're still on top, or a modal was opened outside of our highlight region, we can unhighlight safely - if top_widget == self.show_parent or self[1].dimen:notIntersectWith(UIManager:getPreviousRefreshRegion()) then - UIManager:widgetInvert(self[1], self[1].dimen.x, self[1].dimen.y) - UIManager:setDirty(nil, function() - return "ui", self[1].dimen - end) - else - -- That leaves modals that might have been displayed on top of the highlighted menu entry, in which case, - -- we can't take any shortcuts, as it would invert/paint *over* the popop. - -- Instead, fence the callback to avoid races, and repaint the *full* widget stack properly. - UIManager:waitForVSync() - UIManager:setDirty(self.show_parent, function() - return "ui", self[1].dimen - end) - end - --UIManager:forceRePaint() + UIManager:forceRePaint() end return true end diff --git a/frontend/ui/widget/radiobutton.lua b/frontend/ui/widget/radiobutton.lua index 1466d3fad..5095a6c73 100644 --- a/frontend/ui/widget/radiobutton.lua +++ b/frontend/ui/widget/radiobutton.lua @@ -113,25 +113,29 @@ function RadioButton:onTapCheckButton() if G_reader_settings:isFalse("flash_ui") then self.callback() else - -- While I'd like to only flash the button itself, we have to make do with flashing the full width of the TextWidget... + -- c.f., ui/widget/iconbutton for the canonical documentation about the flash_ui code flow + + -- Highlight + -- + -- self.frame's width is based on self.width, so we effectively flash the full width, not only the button/text's width. + -- This matches the behavior of Menu & TouchMenu. self.frame.invert = true - UIManager:widgetRepaint(self.frame, self.dimen.x, self.dimen.y) - UIManager:setDirty(nil, function() - return "fast", self.dimen - end) + UIManager:widgetInvert(self.frame, self.dimen.x, self.dimen.y) + UIManager:setDirty(nil, "fast", self.dimen) - -- Force the repaint *now*, so we don't have to delay the callback to see the invert... UIManager:forceRePaint() - self.callback() - --UIManager:forceRePaint() -- Unnecessary, the check/uncheck process involves too many repaints already - --UIManager:waitForVSync() + -- Unhighlight + -- self.frame.invert = false - UIManager:widgetRepaint(self.frame, self.dimen.x, self.dimen.y) - UIManager:setDirty(nil, function() - return "fast", self.dimen - end) - --UIManager:forceRePaint() + UIManager:widgetInvert(self.frame, self.dimen.x, self.dimen.y) + UIManager:setDirty(nil, "ui", self.dimen) + + -- Callback + -- + self.callback() + + UIManager:forceRePaint() end elseif self.tap_input then self:onInput(self.tap_input) @@ -157,9 +161,7 @@ function RadioButton:check(callback) self.checked = true self:update() UIManager:widgetRepaint(self.frame, self.dimen.x, self.dimen.y) - UIManager:setDirty(nil, function() - return "fast", self.dimen - end) + UIManager:setDirty(nil, "ui", self.dimen) end function RadioButton:unCheck() @@ -167,9 +169,7 @@ function RadioButton:unCheck() self.checked = false self:update() UIManager:widgetRepaint(self.frame, self.dimen.x, self.dimen.y) - UIManager:setDirty(nil, function() - return "fast", self.dimen - end) + UIManager:setDirty(nil, "ui", self.dimen) end return RadioButton diff --git a/frontend/ui/widget/scrolltextwidget.lua b/frontend/ui/widget/scrolltextwidget.lua index dfbe9faeb..d5932dc01 100644 --- a/frontend/ui/widget/scrolltextwidget.lua +++ b/frontend/ui/widget/scrolltextwidget.lua @@ -40,6 +40,9 @@ local ScrollTextWidget = InputContainer:new{ para_direction_rtl = nil, auto_para_direction = false, alignment_strict = false, + + -- for internal use + for_measurement_only = nil, -- When the widget is a one-off used to compute text height } function ScrollTextWidget:init() @@ -62,6 +65,7 @@ function ScrollTextWidget:init() para_direction_rtl = self.para_direction_rtl, auto_para_direction = self.auto_para_direction, alignment_strict = self.alignment_strict, + for_measurement_only = self.for_measurement_only, } local visible_line_count = self.text_widget:getVisLineCount() local total_line_count = self.text_widget:getAllLineCount() @@ -146,21 +150,26 @@ function ScrollTextWidget:updateScrollBar(is_partial) self.prev_low = low self.prev_high = high self.v_scroll_bar:set(low, high) - local refreshfunc = "ui" - if is_partial then - refreshfunc = "partial" - end - -- Reset transparency if the dialog's MovableContainer is currently translucent... - if is_partial and self.dialog.movable and self.dialog.movable.alpha then - self.dialog.movable.alpha = nil - UIManager:setDirty(self.dialog, function() - return refreshfunc, self.dialog.movable.dimen - end) - else - UIManager:setDirty(self.dialog, function() - return refreshfunc, self.dimen - end) + + -- Don't even try to refresh dummy widgets used for text height computations... + if not self.for_measurement_only then + local refreshfunc = "ui" + if is_partial then + refreshfunc = "partial" + end + -- Reset transparency if the dialog's MovableContainer is currently translucent... + if is_partial and self.dialog.movable and self.dialog.movable.alpha then + self.dialog.movable.alpha = nil + UIManager:setDirty(self.dialog, function() + return refreshfunc, self.dialog.movable.dimen + end) + else + UIManager:setDirty(self.dialog, function() + return refreshfunc, self.dimen + end) + end end + if self.scroll_callback then self.scroll_callback(low, high) end @@ -192,42 +201,42 @@ function ScrollTextWidget:moveCursorToXY(x, y, no_overflow) end function ScrollTextWidget:moveCursorLeft() - self.text_widget:moveCursorLeft(); + self.text_widget:moveCursorLeft() self:updateScrollBar() end function ScrollTextWidget:moveCursorRight() - self.text_widget:moveCursorRight(); + self.text_widget:moveCursorRight() self:updateScrollBar() end function ScrollTextWidget:moveCursorUp() - self.text_widget:moveCursorUp(); + self.text_widget:moveCursorUp() self:updateScrollBar() end function ScrollTextWidget:moveCursorDown() - self.text_widget:moveCursorDown(); + self.text_widget:moveCursorDown() self:updateScrollBar() end function ScrollTextWidget:scrollDown() - self.text_widget:scrollDown(); + self.text_widget:scrollDown() self:updateScrollBar(true) end function ScrollTextWidget:scrollUp() - self.text_widget:scrollUp(); + self.text_widget:scrollUp() self:updateScrollBar(true) end function ScrollTextWidget:scrollToTop() - self.text_widget:scrollToTop(); + self.text_widget:scrollToTop() self:updateScrollBar(true) end function ScrollTextWidget:scrollToBottom() - self.text_widget:scrollToBottom(); + self.text_widget:scrollToBottom() self:updateScrollBar(true) end diff --git a/frontend/ui/widget/spinwidget.lua b/frontend/ui/widget/spinwidget.lua index 70427a216..b83d15477 100644 --- a/frontend/ui/widget/spinwidget.lua +++ b/frontend/ui/widget/spinwidget.lua @@ -234,12 +234,9 @@ function SpinWidget:update() }, self.movable, } - -- If we're translucent, Button itself will handle that post-callback, in order to preserve alpha without flickering. - if not self.movable.alpha then - UIManager:setDirty(self, function() - return "ui", self.spin_frame.dimen - end) - end + UIManager:setDirty(self, function() + return "ui", self.spin_frame.dimen + end) end function SpinWidget:hasMoved() diff --git a/frontend/ui/widget/textboxwidget.lua b/frontend/ui/widget/textboxwidget.lua index e340ad942..e3522d66f 100644 --- a/frontend/ui/widget/textboxwidget.lua +++ b/frontend/ui/widget/textboxwidget.lua @@ -103,6 +103,9 @@ local TextBoxWidget = InputContainer:new{ -- (set to 0 to disable any tab handling and display a tofu glyph) _xtext = nil, -- for internal use _alt_color_for_rtl = nil, -- (for debugging) draw LTR glyphs in black, RTL glyphs in gray + + -- for internal use + for_measurement_only = nil, -- When the widget is a one-off used to compute text height } function TextBoxWidget:init() @@ -1523,6 +1526,9 @@ function TextBoxWidget:moveCursorToCharPos(charpos) if x > self.width - self.cursor_line.dimen.w then x = self.width - self.cursor_line.dimen.w end + if self.for_measurement_only then + return -- we're a dummy widget used for computing text height, don't render/refresh anything + end if not self._bb then return -- no bb yet to render the cursor too end diff --git a/frontend/ui/widget/textviewer.lua b/frontend/ui/widget/textviewer.lua index 40ea1b5f3..602c957f2 100644 --- a/frontend/ui/widget/textviewer.lua +++ b/frontend/ui/widget/textviewer.lua @@ -59,9 +59,6 @@ local TextViewer = InputContainer:new{ text_padding = Size.padding.large, text_margin = Size.margin.small, button_padding = Size.padding.default, - - -- Optional callback called on CloseWidget, set by the widget which showed us (e.g., to request a full-screen refresh) - close_callback = nil, } function TextViewer:init() @@ -232,9 +229,6 @@ function TextViewer:onCloseWidget() UIManager:setDirty(nil, function() return "partial", self.frame.dimen end) - if self.close_callback then - self.close_callback() - end return true end diff --git a/frontend/ui/widget/touchmenu.lua b/frontend/ui/widget/touchmenu.lua index b76961493..2b965e68b 100644 --- a/frontend/ui/widget/touchmenu.lua +++ b/frontend/ui/widget/touchmenu.lua @@ -160,64 +160,34 @@ function TouchMenuItem:onTapSelect(arg, ges) if G_reader_settings:isFalse("flash_ui") then self.menu:onMenuSelect(self.item) else + -- c.f., ui/widget/iconbutton for the canonical documentation about the flash_ui code flow + -- The item frame's width stops at the text width, but we want it to match the menu's length instead local highlight_dimen = self.item_frame.dimen highlight_dimen.w = self.item_frame.width + -- Highlight + -- self.item_frame.invert = true UIManager:widgetInvert(self.item_frame, highlight_dimen.x, highlight_dimen.y, highlight_dimen.w) - UIManager:setDirty(nil, function() - return "fast", highlight_dimen - end) + UIManager:setDirty(nil, "fast", highlight_dimen) - -- Force the repaint *now*, so we don't have to delay the callback to see the invert... - UIManager:forceRePaint() - self.menu:onMenuSelect(self.item) UIManager:forceRePaint() - --UIManager:waitForVSync() + -- Unhighlight + -- self.item_frame.invert = false - -- NOTE: We can *usually* optimize that repaint away, as most entries in the menu will at least trigger a menu repaint ;). - -- But when stuff doesn't repaint the menu and keeps it open, we need to do it. - -- Since it's an *un*highlight containing text, we make it "ui" and not "fast", both so it won't mangle text, - -- and because "fast" can have some weird side-effects on some devices in this specific instance... - if self.item.hold_keep_menu_open or self.item.keep_menu_open then - local top_widget = UIManager:getTopWidget() - -- If the callback opened a full-screen widget, we're done - if top_widget.covers_fullscreen then - return true - end + -- NOTE: If the menu is going to be closed, we can safely drop that. + if self.item.keep_menu_open then + UIManager:widgetInvert(self.item_frame, highlight_dimen.x, highlight_dimen.y, highlight_dimen.w) + UIManager:setDirty(nil, "ui", highlight_dimen) + end - -- If the callback opened the Virtual Keyboard, it gets trickier - -- (this is for TextEditor, Terminal & co) - if top_widget == "VirtualKeyboard" then - -- Unfortunately, we can't really tell full-screen widgets (e.g., TextEditor, Terminal) apart from - -- stuff that might just pop the keyboard for an InputText box... - -- So, a full fenced redraw it is... - UIManager:waitForVSync() - UIManager:setDirty(self.show_parent, function() - return "ui", highlight_dimen - end) - return true - end + -- Callback + -- + self.menu:onMenuSelect(self.item) - -- If we're still on top, or if a modal was opened outside of our highlight region, we can unhighlight safely - if top_widget == self.menu or highlight_dimen:notIntersectWith(UIManager:getPreviousRefreshRegion()) then - UIManager:widgetInvert(self.item_frame, highlight_dimen.x, highlight_dimen.y, highlight_dimen.w) - UIManager:setDirty(nil, function() - return "ui", highlight_dimen - end) - else - -- That leaves modals that might have been displayed on top of the highlighted menu entry, in which case, - -- we can't take any shortcuts, as it would invert/paint *over* the popop. - -- Instead, fence the callback to avoid races, and repaint the *full* widget stack properly. - UIManager:waitForVSync() - UIManager:setDirty(self.show_parent, function() - return "ui", highlight_dimen - end) - end - end - --UIManager:forceRePaint() + UIManager:forceRePaint() end return true end @@ -232,45 +202,35 @@ function TouchMenuItem:onHoldSelect(arg, ges) if G_reader_settings:isFalse("flash_ui") then self.menu:onMenuHold(self.item, self.text_truncated) else + -- c.f., ui/widget/iconbutton for the canonical documentation about the flash_ui code flow + -- The item frame's width stops at the text width, but we want it to match the menu's length instead local highlight_dimen = self.item_frame.dimen highlight_dimen.w = self.item_frame.width + -- Highlight + -- self.item_frame.invert = true UIManager:widgetInvert(self.item_frame, highlight_dimen.x, highlight_dimen.y, highlight_dimen.w) - UIManager:setDirty(nil, function() - return "fast", highlight_dimen - end) + UIManager:setDirty(nil, "fast", highlight_dimen) - -- Force the repaint *now*, so we don't have to delay the callback to see the invert... - UIManager:forceRePaint() - self.menu:onMenuHold(self.item, self.text_truncated) UIManager:forceRePaint() - --UIManager:waitForVSync() + -- Unhighlight + -- self.item_frame.invert = false - -- If the callback closed the menu, we're done. (This field defaults to nil, meaning keep the menu open) - if self.item.hold_keep_menu_open == false then - return true - end - - local top_widget = UIManager:getTopWidget() - -- If we're still on top, or if a modal was opened outside of our highlight region, we can unhighlight safely - if top_widget == self.menu or highlight_dimen:notIntersectWith(UIManager:getPreviousRefreshRegion()) then + -- NOTE: If the menu is going to be closed, we can safely drop that. + -- (This field defaults to nil, meaning keep the menu open, hence the negated test) + if self.item.hold_keep_menu_open ~= false then UIManager:widgetInvert(self.item_frame, highlight_dimen.x, highlight_dimen.y, highlight_dimen.w) - UIManager:setDirty(nil, function() - return "ui", highlight_dimen - end) - else - -- That leaves modals that might have been displayed on top of the highlighted menu entry, in which case, - -- we can't take any shortcuts, as it would invert/paint *over* the popop. - -- Instead, fence the callback to avoid races, and repaint the *full* widget stack properly. - UIManager:waitForVSync() - UIManager:setDirty(self.show_parent, function() - return "ui", highlight_dimen - end) + UIManager:setDirty(nil, "ui", highlight_dimen) end - --UIManager:forceRePaint() + + -- Callback + -- + self.menu:onMenuHold(self.item, self.text_truncated) + + UIManager:forceRePaint() end return true end