mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
e28f45452d
Layout breakpoints use the screen width at 1x, so Interface size scales text, icons and spacing without dropping Home columns or detail panes. Every page, sheet and widget picks a layout that fits instead of cutting text or leaving gaps; search becomes balanced list-row sections, Edit Home keeps hidden widgets, stock KOReader messages become BookOrbit toasts, and the plugin is 2.1.0.
175 lines
6.0 KiB
Lua
175 lines
6.0 KiB
Lua
--[[--
|
|
Pure layout for the widget dashboard: the grid a device gets, the dense
|
|
packing of a page's widgets, and how a page fits the screen.
|
|
|
|
A board is a grid of 2, 4 or 6 columns (by width in dp). Its row height
|
|
stretches between MIN_ROW and MAX_ROW so the page fills the screen exactly;
|
|
a page with too many rows drops widgets from its end, and a page with room to
|
|
spare takes optional extras while rows stay comfortable. Nothing here touches
|
|
widgets, so the whole fit can be unit tested.
|
|
]]
|
|
|
|
local BoardLayout = {}
|
|
|
|
BoardLayout.MIN_ROW = 84
|
|
BoardLayout.COMFY_ROW = 104
|
|
BoardLayout.MAX_ROW = 150
|
|
BoardLayout.GAP = 10
|
|
BoardLayout.TOP = 6
|
|
BoardLayout.HEADER = 64
|
|
BoardLayout.HEADER_GAP = 8
|
|
BoardLayout.FOOTER = 40
|
|
BoardLayout.FOOTER_GAP = 8
|
|
BoardLayout.BOTTOM = 8
|
|
|
|
-- Size codes: S 1x1, M 2x1, L 2x2, W full width x1, XL full width x2. On a
|
|
-- two-column board W is M and XL is L.
|
|
function BoardLayout.sizeDims(code, cols)
|
|
if code == "S" then return 1, 1 end
|
|
if code == "M" then return math.min(2, cols), 1 end
|
|
if code == "L" then return math.min(2, cols), 2 end
|
|
if code == "W" then return cols, 1 end
|
|
return cols, 2
|
|
end
|
|
|
|
function BoardLayout.editBarTop(tile_y, tile_h, bar_h, gap, available_h)
|
|
local below = tile_y + tile_h + gap
|
|
if below + bar_h <= available_h then return below end
|
|
return math.max(0, tile_y - bar_h - gap)
|
|
end
|
|
|
|
function BoardLayout.columnsFor(width_dp)
|
|
if width_dp >= 900 then return 6 end
|
|
if width_dp >= 560 then return 4 end
|
|
return 2
|
|
end
|
|
|
|
function BoardLayout.marginFor(width_dp)
|
|
if width_dp >= 900 then return 28 end
|
|
if width_dp >= 700 then return 24 end
|
|
if width_dp >= 560 then return 20 end
|
|
return 16
|
|
end
|
|
|
|
-- px: the dp-to-pixel function. Returns everything in pixels. width_dp is
|
|
-- the screen's width at interface size 1, so a page keeps its columns at
|
|
-- every interface size.
|
|
function BoardLayout.geometry(px, screen_w, screen_h, width_dp)
|
|
local g = {}
|
|
g.mx = px(BoardLayout.marginFor(width_dp))
|
|
g.w = screen_w - 2 * g.mx
|
|
g.cols = BoardLayout.columnsFor(width_dp)
|
|
g.gap = px(BoardLayout.GAP)
|
|
g.col_w = math.floor((g.w - (g.cols - 1) * g.gap) / g.cols)
|
|
g.top = px(BoardLayout.TOP)
|
|
g.header_h = px(BoardLayout.HEADER)
|
|
g.header_gap = px(BoardLayout.HEADER_GAP)
|
|
g.footer_h = px(BoardLayout.FOOTER)
|
|
g.footer_gap = px(BoardLayout.FOOTER_GAP)
|
|
g.bottom = px(BoardLayout.BOTTOM)
|
|
g.avail = screen_h - g.top - g.header_h - g.header_gap - g.footer_gap - g.footer_h - g.bottom
|
|
g.min_row = px(BoardLayout.MIN_ROW)
|
|
g.comfy_row = px(BoardLayout.COMFY_ROW)
|
|
g.max_row = px(BoardLayout.MAX_ROW)
|
|
return g
|
|
end
|
|
|
|
-- Width of a tile spanning cw columns starting at column x (0-based). The
|
|
-- last column absorbs the rounding remainder so the grid ends on the margin.
|
|
function BoardLayout.spanWidth(g, x, cw)
|
|
if x + cw == g.cols then
|
|
return g.w - x * (g.col_w + g.gap)
|
|
end
|
|
return cw * g.col_w + (cw - 1) * g.gap
|
|
end
|
|
|
|
function BoardLayout.cellX(g, x)
|
|
return x * (g.col_w + g.gap)
|
|
end
|
|
|
|
-- First-fit dense packing, row-major. Returns placed copies (with x, y), the
|
|
-- number of rows used and the number of empty cells within those rows.
|
|
function BoardLayout.pack(items, cols)
|
|
local occ = {}
|
|
local function free(r, c, w, h)
|
|
for y = r, r + h - 1 do
|
|
local line = occ[y]
|
|
if line then
|
|
for x = c, c + w - 1 do
|
|
if line[x] then return false end
|
|
end
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
local rows = 0
|
|
local placed = {}
|
|
for _i, it in ipairs(items) do
|
|
local cw = math.min(it.cw, cols)
|
|
local done = false
|
|
local r = 0
|
|
while not done do
|
|
for x = 0, cols - cw do
|
|
if free(r, x, cw, it.rh) then
|
|
for y = r, r + it.rh - 1 do
|
|
occ[y] = occ[y] or {}
|
|
for xx = x, x + cw - 1 do occ[y][xx] = true end
|
|
end
|
|
rows = math.max(rows, r + it.rh)
|
|
local p = {}
|
|
for k, v in pairs(it) do p[k] = v end
|
|
p.cw, p.x, p.y = cw, x, r
|
|
table.insert(placed, p)
|
|
done = true
|
|
break
|
|
end
|
|
end
|
|
r = r + 1
|
|
end
|
|
end
|
|
local holes = 0
|
|
for y = 0, rows - 1 do
|
|
for x = 0, cols - 1 do
|
|
if not (occ[y] and occ[y][x]) then holes = holes + 1 end
|
|
end
|
|
end
|
|
return { placed = placed, rows = rows, holes = holes }
|
|
end
|
|
|
|
function BoardLayout.rowHeight(g, rows)
|
|
if rows <= 0 then return g.max_row end
|
|
return math.floor((g.avail - (rows - 1) * g.gap) / rows)
|
|
end
|
|
|
|
-- Fits one page: drops trailing widgets until rows are at least MIN_ROW, then
|
|
-- adds extras that cost no row (or keep rows comfortable) and leave no new
|
|
-- holes. Returns the packing plus the row height to draw it at, and the
|
|
-- widgets dropped for room. The first widget always stays: two rows at
|
|
-- MIN_ROW fit every screen.
|
|
function BoardLayout.fitPage(g, base, extras)
|
|
local items = {}
|
|
for _i, it in ipairs(base or {}) do table.insert(items, it) end
|
|
local dropped = {}
|
|
while #items > 1 and BoardLayout.rowHeight(g, BoardLayout.pack(items, g.cols).rows) < g.min_row do
|
|
table.insert(dropped, 1, table.remove(items))
|
|
end
|
|
for _i, ex in ipairs(extras or {}) do
|
|
local before = BoardLayout.pack(items, g.cols)
|
|
local trial = {}
|
|
for _i, it in ipairs(items) do table.insert(trial, it) end
|
|
table.insert(trial, ex)
|
|
local after = BoardLayout.pack(trial, g.cols)
|
|
if (after.rows == before.rows or BoardLayout.rowHeight(g, after.rows) >= g.comfy_row)
|
|
and after.holes <= before.holes then
|
|
items = trial
|
|
end
|
|
end
|
|
local packed = BoardLayout.pack(items, g.cols)
|
|
packed.row_h = math.min(g.max_row, BoardLayout.rowHeight(g, packed.rows))
|
|
packed.grid_h = packed.rows * packed.row_h + math.max(0, packed.rows - 1) * g.gap
|
|
packed.dropped = dropped
|
|
return packed
|
|
end
|
|
|
|
return BoardLayout
|