--[[-- BookOrbit's status pills: one black pill centred above the page's pager. - Toast.show: a short message. A toast in KOReader's sense: any input closes it and still reaches the page underneath. - Toast.error: a failure, with Retry when there is something to retry. Tapping the pill retries; tapping anywhere else only closes it. - Toast.loading: what is loading while a request runs. It stands in for KOReader's TrapWidget, so it is shown before Trapper takes it and any tap, hold, swipe or key cancels the request through dismiss_callback. The pill repaints only its own rectangle, so on e-ink it costs a small partial refresh instead of a dialog's full one. ]] 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 _ = require("gettext") 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 Toast = {} local current = nil -- Above the pager on BookOrbit pages and above KOReader's footer in a book. local function bottomGap() return px(64) end local function maxWidth() local W = BoardMetrics.layoutW() local mx = px(W >= 900 and 28 or W >= 700 and 24 or W >= 560 and 20 or 16) return Screen:getWidth() - 2 * mx end -- The pill for the first of `variants` (lists of runs) that fits the screen. local function buildPill(glyph, variants, light) local h = px(44) local fg = light and INK.black or INK.white local max_w = maxWidth() local chosen for index, parts in ipairs(variants) do local runs = {} for _i, p in ipairs(parts) do table.insert(runs, { t = p.t, b = p.b, col = p.col or fg }) end local line = BoardUI.runs(runs, { s = 15 }) local w = line:getSize().w + px(14) + (glyph and (px(18) + px(10)) or 0) + px(18) if w <= max_w or index == #variants then if w > max_w then line = BoardUI.runs(runs, { s = 15, w = max_w - (w - line:getSize().w), data = true }) w = max_w end chosen = { line = line, w = w } break end if line.free then line:free() end end local items = { BoardUI.space(px(glyph and 14 or 18), 1) } if glyph then table.insert(items, BoardUI.icon(glyph, px(18), { sw = 2.5, col = light and "black" or "white" })) table.insert(items, BoardUI.space(px(10), 1)) end table.insert(items, chosen.line) table.insert(items, BoardUI.flex(0)) local border = light and math.max(1, px(1.5)) or 0 return BoardUI.box({ w = chosen.w, h = h, radius = math.floor(h / 2), border = border, border_col = INK.black, bg = light and INK.white or INK.black }, BoardUI.hstack({ w = chosen.w - 2 * border, h = h - 2 * border }, items)) end local Pill = InputContainer:extend{ name = "bookorbit_toast", kind = "info", glyph = nil, variants = nil, timeout = nil, on_tap = nil, dismiss_callback = nil, } function Pill:init() self[1] = buildPill(self.glyph, self.variants, self.light) local size = self[1]:getSize() local x = math.floor((Screen:getWidth() - size.w) / 2) local y = Screen:getHeight() - (self.bottom or bottomGap()) - size.h self.dimen = Geom:new{ x = x, y = y, w = size.w, h = size.h } self.toast = self.kind == "info" self.modal = self.kind ~= "info" local full = Geom:new{ x = 0, y = 0, w = Screen:getWidth(), h = Screen:getHeight() } self.ges_events = { TapPill = { GestureRange:new{ ges = "tap", range = full } }, HoldPill = { GestureRange:new{ ges = "hold", range = full } }, SwipePill = { GestureRange:new{ ges = "swipe", range = full } }, } if Device:hasKeys() then self.key_events = { AnyKeyPill = { { Device.input.group.Any } } } end end function Pill:paintTo(bb, x, y) self[1]:paintTo(bb, self.dimen.x, self.dimen.y) end function Pill:close() if self.closed then return end self.closed = true if self.timer then UIManager:unschedule(self.timer) end if current == self then current = nil end UIManager:close(self, "ui", self.dimen) end function Pill:onCloseWidget() if self.timer then UIManager:unschedule(self.timer) end if current == self then current = nil end if self[1] and self[1].free then self[1]:free() end end -- A toast closes on any input and lets it through; the others take it. function Pill:handleInput(ges) if self.kind == "info" then self:close() return false end if self.kind == "loading" then if self.dismiss_callback then self.dismiss_callback() end self:close() return true end local on_pill = ges and ges.pos and ges.pos:intersectWith(self.dimen) self:close() if on_pill and self.on_tap then UIManager:nextTick(self.on_tap) end return true end function Pill:onTapPill(_, ges) return self:handleInput(ges) end function Pill:onHoldPill(_, ges) return self:handleInput(ges) end function Pill:onSwipePill(_, ges) return self:handleInput(nil) end function Pill:onAnyKeyPill() return self:handleInput(nil) end function Pill:onGesture(ev) -- A toast must not stop the page from getting the same gesture. if self.toast then self:close() return false end return InputContainer.onGesture(self, ev) end local function show(pill) if current and not current.closed and current.kind ~= "loading" then current:close() end current = pill UIManager:show(pill, "ui", pill.dimen) if pill.timeout then pill.timer = function() pill:close() end UIManager:scheduleIn(pill.timeout, pill.timer) end return pill end -- A short message. o: icon (default check), timeout (s), bottom (px). function Toast.show(str, o) o = o or {} return show(Pill:new{ kind = "info", glyph = o.icon == nil and "check" or o.icon or nil, variants = { { { t = str, b = true } } }, timeout = o.timeout or 2.5, bottom = o.bottom }) end -- A failure. retry: function to run when the pill is tapped, or nil. function Toast.error(str, retry, o) o = o or {} local variants if retry then variants = { { { t = str, b = true }, { t = " " .. _("Retry"), b = true, col = INK.white } }, { { t = str, b = true } }, } else variants = { { { t = str, b = true } } } end return show(Pill:new{ kind = retry and "error" or "info", glyph = "triangle-alert", variants = variants, on_tap = retry, timeout = o.timeout or (retry and 10 or 4), bottom = o.bottom }) end -- What is loading. Returns the pill: hand it to Trapper as its trap widget, -- then close it when the request returns. function Toast.loading(str, o) o = o or {} local hint = INK.gc local pill = Pill:new{ kind = "loading", glyph = "hourglass", variants = { { { t = str, b = true }, { t = " " .. _("Tap anywhere to stop"), col = hint } }, { { t = str, b = true }, { t = " " .. _("Tap to stop"), col = hint } }, { { t = str, b = true } }, }, bottom = o.bottom, } if o.dismissable == false then pill.handleInput = function() return true end end show(pill) UIManager:forceRePaint() return pill end -- Closes whatever pill is up (a page that opens over it). function Toast.dismiss() if current and not current.closed then current:close() end end return Toast