import { HorizontalBox, VerticalBox, ScrollView } from "std-widgets.slint"; import { ZoomControls, ZoomApi } from "zoom-controls.slint"; import { ToolbarIconButton } from "controls.slint"; import { AppTheme, LayoutMetrics } from "../design-system.slint"; import { TypeScale } from "../typography.slint"; component PageNavigation inherits HorizontalBox { in property compact: false; in property busy: false; in property page: 0; in property pages: 0; // A row of its own spreads the arrows to the edges. in property fill: false; callback render-page(int, int); padding: 0px; spacing: 4px; cross-axis-alignment: center; horizontal-stretch: root.fill ? 1 : 0; ToolbarIconButton { compact: root.compact; icon-color: AppTheme.secondary; icon: @image-url("../icons/lucide/outline/chevron-left.svg"); tooltip-text: @tr("Previous page"); interactive: !root.busy && root.page > 0; clicked => { root.render-page(root.page - 1, 200); } } Text { text: @tr("Page {} of {}", root.page + 1, root.pages); color: AppTheme.secondary; font-size: TypeScale.caption; vertical-alignment: center; horizontal-alignment: center; horizontal-stretch: root.fill ? 1 : 0; } ToolbarIconButton { compact: root.compact; icon-color: AppTheme.secondary; icon: @image-url("../icons/lucide/outline/chevron-right.svg"); tooltip-text: @tr("Next page"); interactive: !root.busy && root.page + 1 < root.pages; clicked => { root.render-page(root.page + 1, 200); } } } export component DocumentPreview inherits Rectangle { in property compact: false; in property busy: false; in property source; in property page: 0; in property pages: 0; // Raster resolution can change while its logical page size stays fixed. in property raster-zoom: 1; in property document-revision: 0; callback render-page(int, int); private property fitting: true; private property manual-zoom: 1; private property revision: 0; private property natural-width: root.source.width / max(0.01, root.raster-zoom); private property natural-height: root.source.height / max(0.01, root.raster-zoom); // The page sits as a sheet on the canvas, inset like the email body. private property sheet-gap: root.compact ? 8px : 16px; private property zoom: root.fitting ? clamp((canvas.visible-width - 2 * root.sheet-gap) / 1px / max(1, root.natural-width), 0.1, 3) : root.manual-zoom; changed document-revision => { root.fitting = true; root.manual-zoom = 1; canvas.content-x = 0px; canvas.content-y = 0px; } changed page => { canvas.content-x = 0px; canvas.content-y = 0px; } min-width: 0px; min-height: 0px; max-width: 100000px; horizontal-stretch: 1; vertical-stretch: 1; function command(action: string) { if action == "fit" { root.fitting = true; } else { private-zoom = ZoomApi.resolve(action, root.zoom, 0.1, 3); if private-zoom > 0 { root.manual-zoom = clamp(private-zoom, 0.1, 3); root.fitting = false; } } root.revision += 1; canvas.content-x = 0px; canvas.content-y = 0px; } private property private-zoom: 1; private property toolbar-inset: 10px; private property page-button-size: root.compact ? LayoutMetrics.touch-target : LayoutMetrics.desktop-icon-button-size; // Page navigation shares the zoom row when both fit (four 4px gaps plus // some room between the groups), and takes its own row on narrow // screens or with longer translations. private property pages-below: root.pages > 0 && zoom-controls.preferred-width + 2 * root.page-button-size + page-label-probe.preferred-width + 24px > root.width - 2 * (LayoutMetrics.divider-width + root.toolbar-inset); page-label-probe := Text { visible: false; accessible-role: none; text: @tr("Page {} of {}", root.page + 1, root.pages); font-size: TypeScale.caption; } // Framed like the email reader's message card: controls on top, then // the document on the page background. background: AppTheme.surface; border-radius: 8px; border-width: LayoutMetrics.divider-width; border-color: AppTheme.border-subtle; clip: true; VerticalBox { x: LayoutMetrics.divider-width; y: LayoutMetrics.divider-width; width: parent.width - 2 * LayoutMetrics.divider-width; height: parent.height - 2 * LayoutMetrics.divider-width; padding: 0px; spacing: 0px; cross-axis-alignment: stretch; HorizontalBox { padding-top: 4px; padding-bottom: root.pages-below ? 0px : 4px; padding-left: root.toolbar-inset; padding-right: root.toolbar-inset; spacing: 4px; cross-axis-alignment: center; zoom-controls := ZoomControls { compact: root.compact; zoom: root.zoom; minimum: 0.1; fitting: root.fitting; revision: root.revision; available: root.source.width > 0; command(action) => { root.command(action); } } Rectangle { horizontal-stretch: 1; } if root.pages > 0 && !root.pages-below : PageNavigation { compact: root.compact; busy: root.busy; page: root.page; pages: root.pages; render-page(page, zoom) => { root.render-page(page, zoom); } } } if root.pages-below : PageNavigation { fill: true; padding-left: root.toolbar-inset; padding-right: root.toolbar-inset; padding-bottom: 4px; compact: root.compact; busy: root.busy; page: root.page; pages: root.pages; render-page(page, zoom) => { root.render-page(page, zoom); } } Rectangle { height: LayoutMetrics.divider-width; background: AppTheme.border-subtle; } Rectangle { vertical-stretch: 1; background: AppTheme.page-bg; canvas := ScrollView { width: parent.width; height: parent.height; mouse-drag-pan-enabled: true; content-width: max(self.visible-width, root.natural-width * root.zoom * 1px + 2 * root.sheet-gap); content-height: max(self.visible-height, root.natural-height * root.zoom * 1px + 2 * root.sheet-gap); Rectangle { x: max(root.sheet-gap, (canvas.visible-width - self.width) / 2); y: root.sheet-gap; width: root.natural-width * root.zoom * 1px; height: root.natural-height * root.zoom * 1px; Image { width: parent.width; height: parent.height; source: root.source; image-fit: contain; } // The outline lifts the sheet off the canvas. Rectangle { border-width: LayoutMetrics.divider-width; border-color: AppTheme.border; } } } } } } // Plain-text previews share the document card. export component TextPreview inherits Rectangle { in property text; in property compact: false; private property inset: root.compact ? 12px : 16px; min-width: 0px; min-height: 0px; horizontal-stretch: 1; vertical-stretch: 1; background: AppTheme.surface; border-radius: 8px; border-width: LayoutMetrics.divider-width; border-color: AppTheme.border-subtle; clip: true; scroll := ScrollView { x: LayoutMetrics.divider-width; y: LayoutMetrics.divider-width; width: parent.width - 2 * LayoutMetrics.divider-width; height: parent.height - 2 * LayoutMetrics.divider-width; content-width: self.visible-width; content-height: body.height + 2 * root.inset; body := Text { x: root.inset; y: root.inset; width: scroll.visible-width - 2 * root.inset; height: self.preferred-height; text: root.text; wrap: word-wrap; color: AppTheme.body; font-size: TypeScale.body; } } }