mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
80244ea4e2
Squash of feat/koreader-widget-board. KOReader plugin 2.0.0 gets the widget-board dashboard, redesigned book details, menus, lists and dialogs, Decor page and reading companions. Highlights, notes and bookmarks now stay in step across web, Kobo, KOReader and the phones (defects D-00 to D-20, migration 0121), and reading progress and narration hand off cleanly between devices.
257 lines
9.2 KiB
Lua
257 lines
9.2 KiB
Lua
--[[--
|
|
A full-screen BookOrbit page or sheet, drawn with the dashboard's primitives.
|
|
|
|
The page builds its content with a build function and routes every tap to the
|
|
action of the hit area drawn under the finger, like the board's tiles. A sheet
|
|
keeps a lightened copy of the screen it opened over as its backdrop, so the
|
|
page underneath is never redrawn while the sheet is up and closing it is one
|
|
refresh. On a D-pad device the arrow keys move a focus ring between hit areas
|
|
and Press runs the focused one.
|
|
]]
|
|
|
|
local Blitbuffer = require("ffi/blitbuffer")
|
|
local Device = require("device")
|
|
local Geom = require("ui/geometry")
|
|
local GestureRange = require("ui/gesturerange")
|
|
local InputContainer = require("ui/widget/container/inputcontainer")
|
|
local UIManager = require("ui/uimanager")
|
|
|
|
local BoardMetrics = require("bookorbit_board_metrics")
|
|
local BoardUI = require("bookorbit_board_ui")
|
|
|
|
local Screen = Device.screen
|
|
local INK = BoardMetrics.INK
|
|
local px = BoardMetrics.px
|
|
|
|
local MenuPage = InputContainer:extend{
|
|
covers_fullscreen = true,
|
|
-- function(page, w, h) returning the page's widget.
|
|
build = nil,
|
|
-- function(page, action), for actions the page does not handle itself.
|
|
on_action = nil,
|
|
-- function(page, action) returning true when the page handled Back
|
|
-- itself (a page with a stack of its own); otherwise Back closes it.
|
|
on_back = nil,
|
|
on_close = nil,
|
|
-- A sheet: keep the screen underneath, lightened, as the backdrop.
|
|
backdrop = false,
|
|
-- Taps outside panel_rect (set by build) close the page.
|
|
dismiss_outside = false,
|
|
panel_rect = nil,
|
|
-- function(page, ges) returning true when it used a swipe (paged text).
|
|
on_swipe = nil,
|
|
-- function(page, delta): the page-turn keys, for pages that turn.
|
|
on_page_key = nil,
|
|
name = "bookorbit_menu_page",
|
|
}
|
|
|
|
local SCRIM = 0.62
|
|
|
|
function MenuPage:init()
|
|
self.dimen = Geom:new{ x = 0, y = 0, w = Screen:getWidth(), h = Screen:getHeight() }
|
|
if self.backdrop then
|
|
self.backdrop_bb = Screen.bb:copy()
|
|
self.backdrop_bb:lightenRect(0, 0, self.dimen.w, self.dimen.h, SCRIM)
|
|
end
|
|
self.ges_events = {
|
|
TapPage = { GestureRange:new{ ges = "tap", range = self.dimen } },
|
|
SwipePage = { GestureRange:new{ ges = "swipe", range = self.dimen } },
|
|
}
|
|
self.key_events = self.key_events or {}
|
|
if Device:hasKeys() then
|
|
self.key_events.ClosePage = { { Device.input.group.Back } }
|
|
end
|
|
if self.on_page_key and Device:hasKeys() then
|
|
self.key_events.PageKeyFwd = { { Device.input.group.PgFwd } }
|
|
self.key_events.PageKeyBack = { { Device.input.group.PgBack } }
|
|
end
|
|
if Device:hasDPad() then
|
|
self.key_events.FocusUp = { { "Up" } }
|
|
self.key_events.FocusDown = { { "Down" } }
|
|
self.key_events.FocusLeft = { { "Left" } }
|
|
self.key_events.FocusRight = { { "Right" } }
|
|
self.key_events.PressFocus = { { "Press" } }
|
|
end
|
|
self:rebuild()
|
|
end
|
|
|
|
function MenuPage:rebuild()
|
|
local widget, hits = BoardUI.collectHits(function()
|
|
return self.build(self, self.dimen.w, self.dimen.h)
|
|
end)
|
|
if self[1] and self[1].free then self[1]:free() end
|
|
self[1] = widget
|
|
self.hits = hits
|
|
if self.focus_index and self.focus_index > #hits then self.focus_index = #hits end
|
|
end
|
|
|
|
-- Rebuilds and repaints. The sheet's backdrop never changes, so a rebuild only
|
|
-- needs the panel's rectangle when the panel kept its size.
|
|
function MenuPage:refresh(mode)
|
|
local before = self.panel_rect and self.panel_rect:copy() or nil
|
|
self:rebuild()
|
|
local region = self.dimen
|
|
if before and self.panel_rect and before.h == self.panel_rect.h then region = self.panel_rect end
|
|
UIManager:setDirty(self, function() return mode or "ui", region end)
|
|
end
|
|
|
|
function MenuPage:paintTo(bb, x, y)
|
|
if self.backdrop_bb then
|
|
bb:blitFrom(self.backdrop_bb, 0, 0, 0, 0, self.dimen.w, self.dimen.h)
|
|
else
|
|
bb:paintRect(0, 0, self.dimen.w, self.dimen.h, INK.white)
|
|
end
|
|
self[1]:paintTo(bb, x, y)
|
|
local focused = self.focus_index and self.hits[self.focus_index]
|
|
if focused and focused.dimen then
|
|
local d = focused.dimen
|
|
local ring = math.max(2, px(2.5))
|
|
bb:paintBorder(d.x - ring, d.y - ring, d.w + 2 * ring, d.h + 2 * ring, ring, INK.black, px(8))
|
|
end
|
|
end
|
|
|
|
function MenuPage:onCloseWidget()
|
|
if self.backdrop_bb then
|
|
self.backdrop_bb:free()
|
|
self.backdrop_bb = nil
|
|
end
|
|
if self[1] and self[1].free then self[1]:free() end
|
|
end
|
|
|
|
function MenuPage:close()
|
|
if self.closed then return end
|
|
self.closed = true
|
|
UIManager:close(self, "ui")
|
|
if self.on_close then self.on_close(self) end
|
|
end
|
|
|
|
function MenuPage:onClosePage()
|
|
self:runAction({ kind = "back" })
|
|
return true
|
|
end
|
|
|
|
-- The smallest area under the finger wins, so a button drawn inside a row
|
|
-- (Test inside the server field) is reachable whatever order they were built.
|
|
function MenuPage:hitAt(pos)
|
|
local best, best_index, best_size
|
|
for index = #self.hits, 1, -1 do
|
|
local area = self.hits[index]
|
|
local d = area.dimen
|
|
if d and pos:intersectWith(d) and (not best_size or d.w * d.h < best_size) then
|
|
best, best_index, best_size = area, index, d.w * d.h
|
|
end
|
|
end
|
|
if best then return best, best_index end
|
|
-- A near miss goes to the closest area within a finger's width, which
|
|
-- gives the small pills and stepper buttons a target the size of a tap.
|
|
local slop, best_dist = px(14), nil
|
|
for index, area in ipairs(self.hits) do
|
|
local d = area.dimen
|
|
if d then
|
|
local dx = math.max(d.x - pos.x, 0, pos.x - (d.x + d.w))
|
|
local dy = math.max(d.y - pos.y, 0, pos.y - (d.y + d.h))
|
|
local dist = math.max(dx, dy)
|
|
if dist <= slop and (not best_dist or dist < best_dist) then
|
|
best, best_index, best_dist = area, index, dist
|
|
end
|
|
end
|
|
end
|
|
return best, best_index
|
|
end
|
|
|
|
function MenuPage:onTapPage(_, ges)
|
|
local area = ges and ges.pos and self:hitAt(ges.pos)
|
|
if area then
|
|
self:runAction(area.action)
|
|
elseif self.dismiss_outside and self.panel_rect and ges and ges.pos and not ges.pos:intersectWith(self.panel_rect) then
|
|
self:close()
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- A sheet closes when pushed back up; pages ignore swipes unless they turn.
|
|
function MenuPage:onSwipePage(_, ges)
|
|
if self.on_swipe and ges and self.on_swipe(self, ges) then return true end
|
|
if self.dismiss_outside and ges and ges.direction == "north" then self:close() end
|
|
return true
|
|
end
|
|
|
|
function MenuPage:onPageKeyFwd()
|
|
if self.on_page_key then self.on_page_key(self, 1) end
|
|
return true
|
|
end
|
|
|
|
function MenuPage:onPageKeyBack()
|
|
if self.on_page_key then self.on_page_key(self, -1) end
|
|
return true
|
|
end
|
|
|
|
function MenuPage:runAction(action)
|
|
if not action or action.kind == "noop" then return end
|
|
if action.kind == "back" or action.kind == "close" then
|
|
if action.kind == "back" and self.on_back and self.on_back(self, action) then return end
|
|
self:close()
|
|
return
|
|
end
|
|
if self.on_action then self.on_action(self, action) end
|
|
end
|
|
|
|
-- ------------------------------------------------------------------ focus
|
|
local function centre(d)
|
|
return d.x + d.w / 2, d.y + d.h / 2
|
|
end
|
|
|
|
function MenuPage:moveFocus(dx, dy)
|
|
local hits = self.hits or {}
|
|
if #hits == 0 then return true end
|
|
local current = self.focus_index and hits[self.focus_index]
|
|
local old = current and current.dimen and current.dimen:copy()
|
|
local best, best_score
|
|
if not current or not current.dimen then
|
|
best = 1
|
|
else
|
|
local cx, cy = centre(current.dimen)
|
|
for index, area in ipairs(hits) do
|
|
if index ~= self.focus_index and area.dimen then
|
|
local ax, ay = centre(area.dimen)
|
|
local along = (ax - cx) * dx + (ay - cy) * dy
|
|
if along > 1 then
|
|
local across = math.abs((ax - cx) * dy) + math.abs((ay - cy) * dx)
|
|
local score = along + across * 2
|
|
if not best_score or score < best_score then best, best_score = index, score end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if not best then return true end
|
|
self.focus_index = best
|
|
local ring = math.max(2, px(2.5))
|
|
local function grow(d) return Geom:new{ x = d.x - ring, y = d.y - ring, w = d.w + 2 * ring, h = d.h + 2 * ring } end
|
|
local new = hits[best].dimen
|
|
if old then UIManager:setDirty(self, function() return "fast", grow(old) end) end
|
|
if new then UIManager:setDirty(self, function() return "fast", grow(new) end) end
|
|
return true
|
|
end
|
|
|
|
function MenuPage:onFocusUp() return self:moveFocus(0, -1) end
|
|
function MenuPage:onFocusDown() return self:moveFocus(0, 1) end
|
|
function MenuPage:onFocusLeft() return self:moveFocus(-1, 0) end
|
|
function MenuPage:onFocusRight() return self:moveFocus(1, 0) end
|
|
|
|
function MenuPage:onPressFocus()
|
|
local area = self.focus_index and self.hits[self.focus_index]
|
|
if area then self:runAction(area.action) end
|
|
return true
|
|
end
|
|
|
|
-- Opens a page over whatever is on screen.
|
|
function MenuPage.show(opts)
|
|
local page = MenuPage:new(opts)
|
|
UIManager:show(page, "ui")
|
|
return page
|
|
end
|
|
|
|
MenuPage.Blitbuffer = Blitbuffer
|
|
|
|
return MenuPage
|