import type { DocPageHeaderFooter, DocPageLayout, DocPageSetup } from "./doc-page-setup.ts" import { mmToPx, pxToMm } from "./doc-page-setup.ts" export type DocsHeaderFooterRegion = "header" | "footer" /** Height of the Google Docs–style header/footer chrome bar (px). */ export const DOCS_HF_CHROME_BAR_PX = 28 export function headerOffsetPx(pageLayout: DocPageLayout): number { return pageLayout.headerMarginMm != null ? mmToPx(pageLayout.headerMarginMm) : 0 } export function footerOffsetPx(pageLayout: DocPageLayout): number { return pageLayout.footerMarginMm != null ? mmToPx(pageLayout.footerMarginMm) : 0 } export function defaultHeaderZoneHeightPx(pageLayout: DocPageLayout): number { return Math.max(24, pageLayout.marginsPx.top - headerOffsetPx(pageLayout)) } export function defaultFooterZoneHeightPx(pageLayout: DocPageLayout): number { return Math.max(24, pageLayout.marginsPx.bottom - footerOffsetPx(pageLayout)) } export function regionZoneHeightPx( region: DocPageHeaderFooter | null | undefined, defaultPx: number ): number { if (region?.heightMm != null && region.heightMm > 0) { return Math.max(defaultPx, mmToPx(region.heightMm)) } return defaultPx } /** Header/footer content shown on a given page index. */ export function resolveRegionForPage( pageLayout: DocPageLayout, region: DocsHeaderFooterRegion, pageIndex: number ): DocPageHeaderFooter | null | undefined { const differentFirst = pageLayout.headerFooterDifferentFirstPage ?? false if (region === "header") { if (pageIndex === 0 && differentFirst) return pageLayout.headerFirstPage ?? null return pageLayout.header } if (pageIndex === 0 && differentFirst) return pageLayout.footerFirstPage ?? null return pageLayout.footer } export function effectiveTopMarginPx(pageLayout: DocPageLayout): number { const offset = headerOffsetPx(pageLayout) const defaultZone = defaultHeaderZoneHeightPx(pageLayout) const sharedH = regionZoneHeightPx(pageLayout.header, defaultZone) const firstH = pageLayout.headerFooterDifferentFirstPage ? regionZoneHeightPx(pageLayout.headerFirstPage, defaultZone) : sharedH return Math.max(pageLayout.marginsPx.top, offset + Math.max(sharedH, firstH)) } export function effectiveBottomMarginPx(pageLayout: DocPageLayout): number { const offset = footerOffsetPx(pageLayout) const defaultZone = defaultFooterZoneHeightPx(pageLayout) const sharedH = regionZoneHeightPx(pageLayout.footer, defaultZone) const firstH = pageLayout.headerFooterDifferentFirstPage ? regionZoneHeightPx(pageLayout.footerFirstPage, defaultZone) : sharedH return Math.max(pageLayout.marginsPx.bottom, offset + Math.max(sharedH, firstH)) } export function effectiveMarginsPx( pageLayout: DocPageLayout, livePreview?: { region: DocsHeaderFooterRegion; heightPx: number } | null, measuredHeights?: Partial> ) { let top = effectiveTopMarginPx(pageLayout) let bottom = effectiveBottomMarginPx(pageLayout) if (measuredHeights?.header != null) { const offset = headerOffsetPx(pageLayout) top = Math.max(pageLayout.marginsPx.top, offset + measuredHeights.header) } if (measuredHeights?.footer != null) { const offset = footerOffsetPx(pageLayout) bottom = Math.max(pageLayout.marginsPx.bottom, offset + measuredHeights.footer) } if (livePreview) { const { region, heightPx } = livePreview if (region === "header") { const offset = headerOffsetPx(pageLayout) top = Math.max(pageLayout.marginsPx.top, offset + heightPx) } else { const offset = footerOffsetPx(pageLayout) bottom = Math.max(pageLayout.marginsPx.bottom, offset + heightPx) } } return { top, right: pageLayout.marginsPx.right, bottom, left: pageLayout.marginsPx.left, } } export function pageRegionZoneHeightPx( pageLayout: DocPageLayout, region: DocsHeaderFooterRegion, pageIndex: number ): number { const data = resolveRegionForPage(pageLayout, region, pageIndex) const defaultPx = region === "header" ? defaultHeaderZoneHeightPx(pageLayout) : defaultFooterZoneHeightPx(pageLayout) return regionZoneHeightPx(data, defaultPx) } export function pageHeaderGeometry( pageLayout: DocPageLayout, pageTop: number, pageIndex: number ) { const offset = headerOffsetPx(pageLayout) const zoneHeight = pageRegionZoneHeightPx(pageLayout, "header", pageIndex) const zoneTop = pageTop + offset return { zoneTop, zoneHeight, bottomLine: zoneTop + zoneHeight, offset, } } export function pageFooterGeometry( pageLayout: DocPageLayout, pageTop: number, pageHeight: number, pageIndex: number ) { const offset = footerOffsetPx(pageLayout) const zoneHeight = pageRegionZoneHeightPx(pageLayout, "footer", pageIndex) const zoneBottom = pageTop + pageHeight - offset return { zoneTop: zoneBottom - zoneHeight, zoneHeight, zoneBottom, topLine: zoneBottom - zoneHeight, offset, } } export function regionStorageKey( region: DocsHeaderFooterRegion, pageIndex: number, differentFirstPage: boolean ): keyof DocPageSetup { if (pageIndex === 0 && differentFirstPage) { return region === "header" ? "headerFirstPage" : "footerFirstPage" } return region } export function buildRegionPatch( setup: DocPageSetup, region: DocsHeaderFooterRegion, pageIndex: number, content: Record, contentHeightPx: number ): Partial { const differentFirst = setup.headerFooterDifferentFirstPage ?? false const key = regionStorageKey(region, pageIndex, differentFirst) const topPx = mmToPx(setup.marginsMm.top) const bottomPx = mmToPx(setup.marginsMm.bottom) const headerOff = setup.headerMarginMm != null ? mmToPx(setup.headerMarginMm) : 0 const footerOff = setup.footerMarginMm != null ? mmToPx(setup.footerMarginMm) : 0 const defaultZonePx = region === "header" ? Math.max(24, topPx - headerOff) : Math.max(24, bottomPx - footerOff) const heightMm = pxToMm(Math.max(defaultZonePx, contentHeightPx)) return { [key]: { content, heightMm }, } } export function toggleDifferentFirstPage( setup: DocPageSetup, enabled: boolean ): Partial { if (enabled) { return { headerFooterDifferentFirstPage: true, headerFirstPage: setup.headerFirstPage ?? setup.header ?? null, footerFirstPage: setup.footerFirstPage ?? setup.footer ?? null, } } return { headerFooterDifferentFirstPage: false } }