Skip to main content

ui/
imgview.rs

1// imgview -- SVG pictures and the image viewer (ROADMAP ยง6)
2//
3//   ui.svg(id, &doc, w, h) -> EV        an img_svg::Svg drawn sharp at its layout size: w / h in
4//        DIP (0 and 0: the intrinsic size, one 0: from the aspect ratio); currentColor is the
5//        text colour (icons follow the theme); clickable when id != 0; K_IMAGE record with
6//        the document's <title>.  Text in the SVG is set with the UI's fonts (SvgText).
7//   ui.image_view(id, &mut st, img, flags) -> EV    a zoomable, scrollable view of a bitmap
8//        (ViewImage::Bitmap(&Rc<Surface>, version)) or an SVG (ViewImage::Svg(&Rc<Svg>)),
9//        filling its parent unless next_size says otherwise.  EV_ACT when the zoom or the
10//        position changed (st.zoom, st.cx / cy, st.fit), EV_HOVER, EV_FOCUS, EV_DOUBLE.
11//        Mouse: the wheel zooms at the pointer (IV_WHEEL_SCROLL: the wheel scrolls, Ctrl +
12//        wheel zooms; Shift + wheel scrolls sideways), a left or middle drag pans, Ctrl + drag
13//        draws a rectangle to zoom into, a double click switches between 100 % (at the
14//        pointer) and fit.  Keys (focused): arrows pan an eighth of the view, PgUp / PgDn a
15//        page, Home / End to the top / bottom; + / - (with or without Ctrl) zoom in steps,
16//        Ctrl+0 fits, Ctrl+1 shows 100 %.  Scroll bars at the right and bottom edges when the
17//        image is larger than the view (IV_NO_BARS: none).
18//        Drawing: bitmaps through mip levels (2 x 2 box) and bilinear sampling below 400 %,
19//        nearest neighbour above (IV_SMOOTH: bilinear at every zoom), a pixel grid from 800 %
20//        (IV_NO_GRID: none); SVGs are drawn again at the zoom (only the visible part,
21//        currentColor black, IV_TEXT_COLOR: the text colour).  Transparent parts over a
22//        checkerboard (IV_NO_CHECKER: the view's background); the theme decides its colours
23//        (light, dark, high contrast: plain background).
24//        With a GPU the view's zoom and pan come from tiles while they run (canvas.rs, st.gpu: "image"),
25//        the CPU draws the picture a moment later.
26//   ImageViewState { zoom, cx, cy, fit, label }: zoom 1.0 = 100 % (a bitmap pixel on one
27//        screen pixel, an SVG at its intrinsic size in DIP); (cx, cy) the image point at the
28//        view's centre; fit: the zoom follows the view.  st.set_fit() / set_actual() /
29//        zoom_to(rect) / zoom_by(factor) / zoom_pct().  A new image (another Rc) fits.
30//   SvgText { text, fonts }: img_svg::TextHook on ui's text engine; SvgFonts picks the faces:
31//        families registered with ui.font_register, "monospace" = ui.mono_font_set's font,
32//        everything else the style's fonts (regular / semibold / bold; italic slanted).
33//
34// Ids: id (the view: focus, K_IMAGE with st.label, the SVG's title or "Bild"; value = zoom %),
35// part(id, 1) / part(id, 2) the vertical / horizontal scroll bar.
36
37use alloc::rc::Rc;
38use alloc::string::String;
39use alloc::vec::Vec;
40
41use gfx::path::Path;
42use gfx::xform::Matrix;
43use gfx::{Surface, FMT_ARGB, FMT_PARGB};
44use img_core::mip::MipChain;
45use img_core::resample::{RS_BILINEAR, RS_NEAREST};
46use img_core::xform::{affine, XF_TRANSPARENT};
47use img_svg::{FontSpec, Opts, Svg, TextHook};
48use text::{Gp, Text, FONT_BOLD, FONT_REG, FONT_SEMI};
49
50use crate::canvas::{CanvasContent, CanvasDesc, CanvasGpu, CanvasView};
51use crate::cmd::{Draw, DrawRef, Gfx};
52use crate::consts::*;
53use crate::i18n::tr;
54use crate::keys::*;
55use crate::Ui;
56
57/// image_view flags
58pub const IV_WHEEL_SCROLL: u32 = 1; // the wheel scrolls, Ctrl + wheel zooms
59pub const IV_NO_GRID: u32 = 2; // no pixel grid at high zoom
60pub const IV_SMOOTH: u32 = 4; // bilinear at every zoom (no visible pixels)
61pub const IV_NO_BARS: u32 = 8; // no scroll bars
62pub const IV_NO_CHECKER: u32 = 16; // transparency over the view background
63pub const IV_TEXT_COLOR: u32 = 32; // an SVG's currentColor is the text colour (else black)
64
65const ZOOM_MIN: f64 = 0.01;
66const ZOOM_MAX: f64 = 64.0;
67// the steps of + / - (and of the zoom box of apps)
68const STEPS: [f64; 26] = [
69    0.01, 0.02, 0.03, 0.05, 0.08, 0.12, 0.17, 0.25, 0.33, 0.5, 0.67, 0.75, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0, 6.0, 8.0, 12.0, 16.0, 24.0,
70    32.0, 48.0, 64.0,
71];
72const NEAREST_FROM: f64 = 4.0; // device px per image px: pixels become visible
73const GRID_FROM: f64 = 8.0;
74const SVG_CACHE: usize = 64; // Draw objects of ui.svg kept
75
76fn part(id: u32, k: u32) -> u32 {
77    id.wrapping_mul(8).wrapping_add(k) | 0x8000_0000
78}
79
80// FNV-1a over u64 words (the draw objects' identity)
81struct Fnv(u64);
82
83impl Fnv {
84    fn new() -> Fnv {
85        Fnv(0xCBF2_9CE4_8422_2325)
86    }
87    fn u64(&mut self, v: u64) {
88        for b in v.to_le_bytes() {
89            self.0 ^= b as u64;
90            self.0 = self.0.wrapping_mul(0x0000_0100_0000_01B3);
91        }
92    }
93}
94
95// ------------------------------------------------------------------ fonts for SVG text
96
97/// the faces SvgText may use (taken from the Ui at frame time)
98#[derive(Clone, Default, Debug)]
99pub struct SvgFonts {
100    /// (family lower case, weight, italic, font id) of ui.font_register
101    pub faces: Vec<(String, u32, bool, u32)>,
102    /// "monospace"
103    pub mono: Option<u32>,
104}
105
106/// an img_svg::TextHook on a text engine
107pub struct SvgText<'a> {
108    pub text: &'a mut Text,
109    pub fonts: &'a SvgFonts,
110}
111
112impl SvgText<'_> {
113    // (font id, slant synthetically)
114    fn pick(&self, f: &FontSpec) -> (u32, bool) {
115        for fam in f.family.split(',') {
116            let fam = fam.trim().trim_matches(|c| c == '"' || c == '\'').to_lowercase();
117            if fam == "monospace" {
118                if let Some(m) = self.fonts.mono.filter(|&m| self.text.valid(m)) {
119                    return (m, f.italic);
120                }
121            }
122            let best = self
123                .fonts
124                .faces
125                .iter()
126                .filter(|x| x.0 == fam && self.text.valid(x.3))
127                .min_by_key(|x| (x.2 != f.italic) as u32 * 10000 + (x.1 as i32 - f.weight as i32).unsigned_abs());
128            if let Some(b) = best {
129                return (b.3, f.italic && !b.2);
130            }
131        }
132        let font = if f.weight >= 600 {
133            FONT_BOLD
134        } else if f.weight >= 500 {
135            FONT_SEMI
136        } else {
137            FONT_REG
138        };
139        (font, f.italic)
140    }
141
142    // advance (px) of s; with a path: the outlines appended (baseline at x, y)
143    fn run(&mut self, s: &str, f: &FontSpec, px: f32, mut out: Option<(&mut Path, f32, f32)>) -> f32 {
144        let (font, slant) = self.pick(f);
145        let t = &mut *self.text;
146        let n0 = out.as_mut().map_or(0, |o| o.0.pts_mut().len());
147        let mut pen = 0.0f32;
148        let mut prev: Option<(u32, u32)> = None;
149        for ch in s.chars() {
150            let cp = ch as u32;
151            let mut ff = font;
152            if !t.has(font, cp) {
153                if let Some(alt) = t.fallback_for(cp) {
154                    ff = alt;
155                }
156            }
157            let g = t.glyph(ff, cp);
158            let upem = t.upem(ff).max(1) as f32;
159            let sx = px / upem;
160            if let Some((pf, pg)) = prev {
161                if pf == ff {
162                    pen += t.kern(ff, pg, g) as f32 * sx;
163                }
164            }
165            if let Some((path, x, y)) = out.as_mut() {
166                t.gp = Gp { x: *x + pen, y: *y, sx, sy: sx, dx: 0, dy: 0 };
167                core::mem::swap(&mut t.path, *path);
168                t.glyph_path(ff, g);
169                core::mem::swap(&mut t.path, *path);
170            }
171            pen += t.adv(ff, g) as f32 * sx;
172            prev = Some((ff, g));
173        }
174        // a synthetic slant (12 degrees) about the baseline
175        if slant {
176            if let Some((path, _, y)) = out {
177                for p in path.pts_mut()[n0..].iter_mut() {
178                    p.0 += (y - p.1) * 0.21;
179                }
180            }
181        }
182        pen
183    }
184}
185
186impl TextHook for SvgText<'_> {
187    fn outline(&mut self, path: &mut Path, s: &str, font: &FontSpec, px: f32, x: f32, y: f32) -> f32 {
188        self.run(s, font, px, Some((path, x, y)))
189    }
190    fn advance(&mut self, s: &str, font: &FontSpec, px: f32) -> f32 {
191        self.run(s, font, px, None)
192    }
193}
194
195// ------------------------------------------------------------------ ui.svg
196
197/// the shared state: the Draw objects of ui.svg by (document, size, colour)
198#[derive(Default)]
199pub(crate) struct S {
200    svgs: Vec<(u64, Rc<SvgDraw>)>,
201    fonts: Option<Rc<SvgFonts>>,
202}
203
204/// an SVG drawn at render time into its rectangle (the rectangle is the viewport)
205pub(crate) struct SvgDraw {
206    pub(crate) doc: Rc<Svg>,
207    pub(crate) color: u32,
208    pub(crate) fonts: Rc<SvgFonts>,
209}
210
211impl Draw for SvgDraw {
212    fn draw(&self, g: &mut Gfx, r: [i32; 4]) {
213        let (x, y) = (r[0] + g.dc.ox, r[1] + g.dc.oy);
214        let clip = g.dc.clip;
215        let mut th = SvgText { text: &mut *g.text, fonts: &self.fonts };
216        let mut o = Opts { color: self.color, text: Some(&mut th) };
217        self.doc.render_rect(g.sf, x as f64, y as f64, r[2] as f64, r[3] as f64, Some(clip), &mut o);
218    }
219}
220
221impl Ui {
222    /// the faces for SVG text (registered families, the monospace font); shared until the
223    /// registered fonts change
224    pub fn svg_fonts(&mut self) -> Rc<SvgFonts> {
225        let mut faces: Vec<(String, u32, bool, u32)> = Vec::new();
226        for fam in self.font_registered() {
227            for w in [100, 200, 300, 400, 500, 600, 700, 800, 900] {
228                for it in [false, true] {
229                    if let Some(id) = self.font_registered_face(&fam, w, it) {
230                        faces.push((fam.to_lowercase(), w, it, id));
231                    }
232                }
233            }
234        }
235        let mono = self.m.term.mono;
236        if let Some(f) = &self.m.svg.fonts {
237            if f.faces == faces && f.mono == mono {
238                return f.clone();
239            }
240        }
241        let f = Rc::new(SvgFonts { faces, mono });
242        self.m.svg.fonts = Some(f.clone());
243        f
244    }
245
246    /// ui_svg(id, doc, w, h) -> EV: an SVG at w x h DIP (see the module header of imgview.rs)
247    #[cfg_attr(jfgui_dev, track_caller)]
248    pub fn svg(&mut self, id: u32, doc: &Rc<Svg>, w: i32, h: i32) -> u32 {
249        let _src = self.src_scope();
250        let (iw, ih) = doc.size();
251        let (w, h) = match (w > 0, h > 0) {
252            (true, true) => (w as f64, h as f64),
253            (true, false) => (w as f64, w as f64 * ih / iw),
254            (false, true) => (h as f64 * iw / ih, h as f64),
255            (false, false) => (iw, ih),
256        };
257        let pw = self.dp((w + 0.5).clamp(1.0, 1.0e5) as i32);
258        let ph = self.dp((h + 0.5).clamp(1.0, 1.0e5) as i32);
259        let r = self.place(pw, ph);
260        let disabled = self.wp.ctx.disabled_depth != 0;
261        let color = self.color(if disabled { T_TEXT_DISABLED } else { T_TEXT });
262        let fonts = self.svg_fonts();
263        let mut hh = Fnv::new();
264        hh.u64(Rc::as_ptr(doc) as usize as u64);
265        hh.u64(color as u64);
266        hh.u64(Rc::as_ptr(&fonts) as usize as u64);
267        let key = hh.0;
268        let s = &mut self.m.svg;
269        // forget the objects of documents nobody else holds
270        s.svgs.retain(|(_, d)| Rc::strong_count(&d.doc) > 1);
271        let d = match s.svgs.iter().find(|(k, _)| *k == key) {
272            Some((_, d)) => d.clone(),
273            None => {
274                if s.svgs.len() >= SVG_CACHE {
275                    s.svgs.remove(0);
276                }
277                let d = Rc::new(SvgDraw { doc: doc.clone(), color, fonts });
278                s.svgs.push((key, d.clone()));
279                d
280            }
281        };
282        let obj: DrawRef = d;
283        self.ltr_begin(r[0], r[2]);
284        self.cmd_custom(r[0], r[1], r[2], r[3], Some(obj), 0, key as u32);
285        self.ltr_end();
286        let ev = if id != 0 { self.behave(id, 0) } else { 0 };
287        let title = doc.title();
288        let label = if title.is_empty() { None } else { Some(title) };
289        if id != 0 || label.is_some() {
290            self.sem(K_IMAGE, id, label, 0);
291        }
292        ev
293    }
294}
295
296// ------------------------------------------------------------------ the image viewer
297
298/// what ui.image_view shows
299#[derive(Clone, Copy)]
300#[non_exhaustive]
301pub enum ViewImage<'a> {
302    /// a bitmap (FMT_PARGB, FMT_ARGB or FMT_XRGB) and its version (bump it when the
303    /// pixels change)
304    Bitmap(&'a Rc<Surface>, u32),
305    Svg(&'a Rc<Svg>),
306}
307
308#[derive(Clone, Copy, Debug, Default, PartialEq)]
309enum Gesture {
310    #[default]
311    Idle,
312    /// panning since the pointer was at (mx, my) and the centre at (cx, cy)
313    Pan { mx: i32, my: i32, cx: f64, cy: f64, middle: bool },
314    /// a zoom rectangle from (x, y) px
315    Rect { x: i32, y: i32 },
316}
317
318/// the app-owned state of an image view (see the module header of imgview.rs)
319#[derive(Clone)]
320pub struct ImageViewState {
321    /// 1.0 = 100 %
322    pub zoom: f64,
323    /// the image point (image px) at the centre of the view
324    pub cx: f64,
325    pub cy: f64,
326    /// the zoom follows the view: the whole image, centred
327    pub fit: bool,
328    /// the accessible name (empty: the SVG's title, else "Bild")
329    pub label: String,
330    /// how the view's zoom and pan use the GPU (canvas.rs)
331    pub gpu: CanvasGpu,
332    key: usize,
333    ver: u32,
334    // a PARGB copy of an XRGB bitmap and the mip levels
335    conv: Option<Rc<Surface>>,
336    mips: Option<Rc<MipChain>>,
337    g: Gesture,
338    draw: Option<(u64, Rc<IvDraw>)>,
339    // the last frame: view size (px), device px per image px at zoom 1
340    view: [i32; 2],
341    unit: f64,
342}
343
344impl Default for ImageViewState {
345    fn default() -> ImageViewState {
346        ImageViewState {
347            zoom: 1.0,
348            cx: 0.0,
349            cy: 0.0,
350            fit: true,
351            label: String::new(),
352            gpu: CanvasGpu::Zoom,
353            key: 0,
354            ver: 0,
355            conv: None,
356            mips: None,
357            g: Gesture::Idle,
358            draw: None,
359            view: [0, 0],
360            unit: 1.0,
361        }
362    }
363}
364
365impl core::fmt::Debug for ImageViewState {
366    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
367        f.debug_struct("ImageViewState").field("zoom", &self.zoom).field("cx", &self.cx).field("cy", &self.cy).field("fit", &self.fit).finish()
368    }
369}
370
371impl ImageViewState {
372    /// the zoom in percent (rounded)
373    pub fn zoom_pct(&self) -> i32 {
374        (self.zoom * 100.0 + 0.5) as i32
375    }
376
377    /// fit mode: the whole image in the view
378    pub fn set_fit(&mut self) {
379        self.fit = true;
380    }
381
382    /// 100 %, the centre kept
383    pub fn set_actual(&mut self) {
384        self.fit = false;
385        self.zoom = 1.0;
386    }
387
388    /// multiply the zoom (about the view's centre)
389    pub fn zoom_by(&mut self, f: f64) {
390        if f > 0.0 {
391            self.fit = false;
392            self.zoom = (self.zoom * f).clamp(ZOOM_MIN, ZOOM_MAX);
393        }
394    }
395
396    /// show the image rectangle x, y, w, h (image px) as large as the view allows
397    pub fn zoom_to(&mut self, r: [f64; 4]) {
398        let (vw, vh) = (self.view[0] as f64, self.view[1] as f64);
399        if !(r[2] > 0.0 && r[3] > 0.0) || vw <= 0.0 || vh <= 0.0 {
400            return;
401        }
402        let u = if self.unit > 0.0 { self.unit } else { 1.0 };
403        self.fit = false;
404        self.zoom = (vw / (r[2] * u)).min(vh / (r[3] * u)).clamp(ZOOM_MIN, ZOOM_MAX);
405        self.cx = r[0] + r[2] * 0.5;
406        self.cy = r[1] + r[3] * 0.5;
407    }
408
409    // the next step of the zoom list in direction dir (+1 / -1)
410    fn step(&mut self, dir: i32) {
411        let z = self.zoom;
412        let next = if dir > 0 { STEPS.iter().copied().find(|&s| s > z * 1.001) } else { STEPS.iter().rev().copied().find(|&s| s < z * 0.999) };
413        if let Some(n) = next {
414            self.fit = false;
415            self.zoom = n;
416        }
417    }
418
419    // zoom to z keeping the image point under the view point (px, py) relative to the view's
420    // centre in place
421    fn zoom_at(&mut self, z: f64, dx: f64, dy: f64) {
422        let s0 = self.zoom * self.unit;
423        let z = z.clamp(ZOOM_MIN, ZOOM_MAX);
424        let s1 = z * self.unit;
425        if !(s0 > 0.0 && s1 > 0.0) {
426            return;
427        }
428        let (px, py) = (self.cx + dx / s0, self.cy + dy / s0);
429        self.fit = false;
430        self.zoom = z;
431        self.cx = px - dx / s1;
432        self.cy = py - dy / s1;
433    }
434}
435
436/// the view's drawing (render time)
437struct IvDraw {
438    img: IvImg,
439    mips: Option<Rc<MipChain>>,
440    s: f64,
441    ox: f64,
442    oy: f64,
443    iw: f64,
444    ih: f64,
445    flags: u32,
446    bg: u32,
447    checker: Option<(i32, u32, u32)>,
448    grid: u32,
449    color: u32,
450    fonts: Rc<SvgFonts>,
451}
452
453enum IvImg {
454    Bitmap(Rc<Surface>),
455    Svg(Rc<Svg>),
456}
457
458impl IvDraw {
459    // the bitmap through the mips and the sampler into the rows y0 .. y1 of x0 .. x1
460    fn bitmap(&self, g: &mut Gfx, src0: &Surface, a: [i32; 4], v: CanvasView) {
461        let mut src = src0;
462        let mut s = v.scale;
463        let mut level = 1;
464        if let Some(m) = &self.mips {
465            while s < 0.5 {
466                match m.level(level) {
467                    Some(l) => {
468                        src = l;
469                        s *= 2.0;
470                        level += 1;
471                    }
472                    None => break,
473                }
474            }
475        }
476        let sx = src.w as f64 / src0.w.max(1) as f64; // level px per image px
477        let sy = src.h as f64 / src0.h.max(1) as f64;
478        let filter = if v.scale >= NEAREST_FROM && self.flags & IV_SMOOTH == 0 { RS_NEAREST } else { RS_BILINEAR };
479        let w = a[2] - a[0];
480        const BAND: i32 = 64;
481        let mut tile = Surface::new(w, BAND, FMT_PARGB);
482        if tile.px.is_empty() {
483            return;
484        }
485        let stride = g.sf.stride as usize;
486        let mut y = a[1];
487        while y < a[3] {
488            let n = (a[3] - y).min(BAND);
489            // tile (tx, ty) = device (tx + a[0], ty + y) -> level px
490            let m = Matrix { a: sx / v.scale, b: 0.0, c: (a[0] as f64 - v.ox) * sx / v.scale, d: 0.0, e: sy / v.scale, f: (y as f64 - v.oy) * sy / v.scale };
491            if affine(src, &mut tile, &m, filter, XF_TRANSPARENT, Some([0, 0, w, n])) != 0 {
492                return;
493            }
494            for r in 0..n {
495                let to = r as usize * tile.stride as usize;
496                let trow = &tile.px[to..to + w as usize * 4];
497                let d = (y + r) as usize * stride + a[0] as usize * 4;
498                let Some(drow) = g.sf.px.get_mut(d..d + w as usize * 4) else { continue };
499                match self.checker {
500                    Some((cell, c0, c1)) => {
501                        let cfg = gfx::straight::ViewCfg { cell, c0, c1 };
502                        gfx::straight::view_row_pm(drow, trow, a[0] - v.ox as i32, y + r - v.oy as i32, &cfg);
503                    }
504                    None => gfx::blend::blit_over(drow, trow, 255),
505                }
506            }
507            y += n;
508        }
509    }
510
511    // the checkerboard under an SVG
512    fn checker_fill(&self, g: &mut Gfx, a: [i32; 4], v: CanvasView) {
513        let Some((cell, c0, c1)) = self.checker else { return };
514        let cfg = gfx::straight::ViewCfg { cell, c0, c1 };
515        let stride = g.sf.stride as usize;
516        for y in a[1]..a[3] {
517            let d = y as usize * stride + a[0] as usize * 4;
518            let Some(row) = g.sf.px.get_mut(d..d + (a[2] - a[0]) as usize * 4) else { continue };
519            for (i, px) in row.chunks_exact_mut(4).enumerate() {
520                let c = gfx::straight::view_bg(a[0] + i as i32 - v.ox as i32, y - v.oy as i32, &cfg);
521                px.copy_from_slice(&c.to_le_bytes());
522            }
523        }
524    }
525
526    // pixel boundaries at high zoom
527    fn grid(&self, g: &mut Gfx, a: [i32; 4], v: CanvasView) {
528        let (dx, dy) = (g.dc.ox, g.dc.oy);
529        g.dc.set_color(self.grid);
530        let i0 = ((a[0] as f64 - v.ox) / v.scale).max(0.0) as i64;
531        let i1 = (((a[2] as f64 - v.ox) / v.scale) as i64 + 1).min(self.iw as i64);
532        for i in i0..=i1 {
533            let x = (v.ox + i as f64 * v.scale).rnd() as i32;
534            if x >= a[0] && x < a[2] {
535                g.dc.fill(g.sf, x - dx, a[1] - dy, 1, a[3] - a[1]);
536            }
537        }
538        let j0 = ((a[1] as f64 - v.oy) / v.scale).max(0.0) as i64;
539        let j1 = (((a[3] as f64 - v.oy) / v.scale) as i64 + 1).min(self.ih as i64);
540        for j in j0..=j1 {
541            let y = (v.oy + j as f64 * v.scale).rnd() as i32;
542            if y >= a[1] && y < a[3] {
543                g.dc.fill(g.sf, a[0] - dx, y - dy, a[2] - a[0], 1);
544            }
545        }
546    }
547}
548
549impl Draw for IvDraw {
550    fn draw(&self, g: &mut Gfx, r: [i32; 4]) {
551        self.paint(g, r, CanvasView { scale: self.s, ox: self.ox, oy: self.oy });
552    }
553}
554
555impl CanvasContent for IvDraw {
556    fn draw_view(&self, g: &mut Gfx, v: CanvasView, rect: [i32; 4]) {
557        self.paint(g, rect, v);
558    }
559}
560
561impl IvDraw {
562    // background and image through view v in the clip
563    fn paint(&self, g: &mut Gfx, r: [i32; 4], v: CanvasView) {
564        let (dx, dy) = (g.dc.ox, g.dc.oy);
565        let clip = g.dc.clip;
566        let view = [r[0] + dx, r[1] + dy, r[0] + dx + r[2], r[1] + dy + r[3]];
567        let k = [clip[0].max(view[0]), clip[1].max(view[1]), clip[2].min(view[2]), clip[3].min(view[3])];
568        if k[2] <= k[0] || k[3] <= k[1] {
569            return;
570        }
571        // the view's background
572        g.dc.set_color(self.bg);
573        g.dc.fill(g.sf, k[0] - dx, k[1] - dy, k[2] - k[0], k[3] - k[1]);
574        // the image's pixels inside the clip
575        let ir = [
576            v.ox.max(-1.0e9) as i64,
577            v.oy.max(-1.0e9) as i64,
578            (v.ox + self.iw * v.scale).min(1.0e9).ceil_i64(),
579            (v.oy + self.ih * v.scale).min(1.0e9).ceil_i64(),
580        ];
581        let a = [
582            (ir[0].max(k[0] as i64)) as i32,
583            (ir[1].max(k[1] as i64)) as i32,
584            (ir[2].min(k[2] as i64)) as i32,
585            (ir[3].min(k[3] as i64)) as i32,
586        ];
587        if a[2] <= a[0] || a[3] <= a[1] {
588            return;
589        }
590        match &self.img {
591            IvImg::Bitmap(sf) => {
592                self.bitmap(g, sf, a, v);
593                if v.scale >= GRID_FROM && self.flags & IV_NO_GRID == 0 {
594                    self.grid(g, a, v);
595                }
596            }
597            IvImg::Svg(doc) => {
598                self.checker_fill(g, a, v);
599                let mut th = SvgText { text: &mut *g.text, fonts: &self.fonts };
600                let mut o = Opts { color: self.color, text: Some(&mut th) };
601                let m = Matrix { a: v.scale, b: 0.0, c: v.ox, d: 0.0, e: v.scale, f: v.oy };
602                doc.render(g.sf, &m, Some(a), &mut o);
603            }
604        }
605    }
606}
607
608// rounding without libm
609trait Round {
610    fn ceil_i64(self) -> i64;
611    fn rnd(self) -> f64;
612}
613
614impl Round for f64 {
615    fn ceil_i64(self) -> i64 {
616        let t = self as i64;
617        if (t as f64) < self { t + 1 } else { t }
618    }
619    // to the nearest integer (halves up); beyond 2^52 unchanged
620    fn rnd(self) -> f64 {
621        if !(self.abs() < 4.0e15) {
622            return self;
623        }
624        let v = self + 0.5;
625        let t = v as i64 as f64;
626        if t > v { t - 1.0 } else { t }
627    }
628}
629
630impl Ui {
631    // an unconsumed key vk whose modifiers & mask == need: consumed (arrows as drawn: the
632    // view is not mirrored)
633    fn iv_key(&mut self, vk: u32, need: u32, mask: u32) -> bool {
634        let rtl = self.rtl;
635        for k in self.wp.ctx.keyq.iter_mut() {
636            if (*k as i32) < 0 {
637                continue;
638            }
639            let mods = (*k >> 16) & 0x7FFF;
640            let mut v = *k & 0xFFFF;
641            if rtl && mods & (MOD_CTRL | MOD_ALT) == 0 {
642                v = match v {
643                    VK_LEFT => VK_RIGHT,
644                    VK_RIGHT => VK_LEFT,
645                    v => v,
646                };
647            }
648            if v == vk && mods & mask == need {
649                *k |= 0x8000_0000;
650                return true;
651            }
652        }
653        false
654    }
655
656    /// ui_image_view(id, st, img, flags) -> EV: the image viewer (see the module header of
657    /// imgview.rs)
658    #[cfg_attr(jfgui_dev, track_caller)]
659    pub fn image_view(&mut self, id: u32, st: &mut ImageViewState, img: ViewImage, flags: u32) -> u32 {
660        let _src = self.src_scope();
661        let _ = self.take_flags();
662        if self.wp.lay.next_w == 0 && self.wp.lay.next_h == 0 && self.wp.lay.next_rect_set == 0 {
663            self.next_size(-1, -1);
664        }
665        let r = self.place(0, 0);
666        let dpi = self.wp.rt.dpi;
667        // the image: its size, the device px per image px at 100 %, a new one fits
668        let (key, ver, iw, ih, unit) = match img {
669            ViewImage::Bitmap(sf, v) => (Rc::as_ptr(sf) as usize, v, sf.w.max(0) as f64, sf.h.max(0) as f64, 1.0),
670            ViewImage::Svg(d) => {
671                let (w, h) = d.size();
672                (Rc::as_ptr(d) as usize, 0, w, h, dpi as f64 / 96.0)
673            }
674        };
675        if key != st.key {
676            if st.key != 0 {
677                st.fit = true;
678            }
679            st.key = key;
680            st.ver = ver.wrapping_add(1); // the bitmap's derived data below is built again
681            st.g = Gesture::Idle;
682        }
683        if let ViewImage::Bitmap(sf, v) = img {
684            if st.ver != v {
685                st.ver = v;
686                st.conv = None;
687                st.mips = None;
688                if sf.fmt != FMT_PARGB && sf.fmt != FMT_ARGB && sf.w > 0 && sf.h > 0 {
689                    // XRGB (opaque) -> PARGB
690                    let mut c = Surface::new(sf.w, sf.h, FMT_PARGB);
691                    let n = c.px.len();
692                    if n > 0 && c.stride == sf.stride && sf.px.len() >= n {
693                        c.px.copy_from_slice(&sf.px[..n]);
694                        for p in c.px.chunks_exact_mut(4) {
695                            p[3] = 255;
696                        }
697                        st.conv = Some(Rc::new(c));
698                    }
699                }
700            }
701        } else {
702            st.conv = None;
703            st.mips = None;
704        }
705        st.unit = unit;
706        st.view = [r[2].max(0), r[3].max(0)];
707        let (vw, vh) = (r[2].max(1) as f64, r[3].max(1) as f64);
708        let has = iw > 0.0 && ih > 0.0;
709        let fitz = if has { (vw / (iw * unit)).min(vh / (ih * unit)).clamp(ZOOM_MIN, ZOOM_MAX) } else { 1.0 };
710        let view0 = (st.zoom, st.cx, st.cy, st.fit);
711        // ---- input
712        let mut cur = CUR_MOVE;
713        if matches!(st.g, Gesture::Rect { .. }) || self.mods & MOD_CTRL != 0 {
714            cur = CUR_CROSS;
715        }
716        let ev = self.behave(id, HB_FOCUS | HB_MOVES | HB_GRAB_H | HB_GRAB_V | cur << HB_CURSOR_SHIFT);
717        let state = self.wp.ctx.last_state;
718        let (mx, my) = (self.rtl_map(self.wp.ctx.mx, r[0], r[2]), self.wp.ctx.my);
719        let (cxv, cyv) = (r[0] as f64 + vw * 0.5, r[1] as f64 + vh * 0.5);
720        if st.fit {
721            st.zoom = fitz;
722            st.cx = iw * 0.5;
723            st.cy = ih * 0.5;
724        }
725        st.zoom = st.zoom.clamp(ZOOM_MIN, ZOOM_MAX);
726        if has {
727            // the wheel
728            let c = &self.wp.ctx;
729            let (wx, wy) = if ev & EV_HOVER != 0 { (c.wheel_x, c.wheel_y) } else { (0, 0) };
730            if wx != 0 || wy != 0 {
731                let ctrl = self.mods & MOD_CTRL != 0;
732                let shift = self.mods & MOD_SHIFT != 0;
733                let zoom = if flags & IV_WHEEL_SCROLL != 0 { ctrl } else { !shift };
734                let s = st.zoom * unit;
735                let notch = self.dp(48) as f64;
736                if zoom && wy != 0 {
737                    let f = rt::x87::pow(1.25, wy as f64 / 120.0);
738                    st.zoom_at(st.zoom * f, mx as f64 + 0.5 - cxv, my as f64 + 0.5 - cyv);
739                } else {
740                    // wheel up shows the upper part (Shift: the left part)
741                    let (mut ddx, mut ddy) = (wx as f64 / 120.0 * notch, -(wy as f64) / 120.0 * notch);
742                    if shift {
743                        ddx += ddy;
744                        ddy = 0.0;
745                    }
746                    if self.rtl {
747                        ddx = -ddx;
748                    }
749                    st.fit = false;
750                    st.cx += ddx / s;
751                    st.cy += ddy / s;
752                }
753                if zoom && wx != 0 {
754                    st.fit = false;
755                    st.cx += wx as f64 / 120.0 * notch / s;
756                }
757                let c = &mut self.wp.ctx;
758                c.wheel_x = 0;
759                c.wheel_y = 0;
760            }
761            // presses: pan (left, middle), zoom rectangle (Ctrl + left)
762            let c = &self.wp.ctx;
763            let hot = c.hot == id;
764            let s = st.zoom * unit;
765            if hot && c.pressed & (1 << BTN_LEFT) != 0 && c.active == id {
766                st.g = if self.mods & MOD_CTRL != 0 { Gesture::Rect { x: mx, y: my } } else { Gesture::Pan { mx, my, cx: st.cx, cy: st.cy, middle: false } };
767            } else if hot && c.pressed & (1 << BTN_MIDDLE) != 0 {
768                st.g = Gesture::Pan { mx, my, cx: st.cx, cy: st.cy, middle: true };
769            }
770            let c = &self.wp.ctx;
771            match st.g {
772                Gesture::Pan { mx: x0, my: y0, cx, cy, middle } => {
773                    let held = if middle { c.mbtn & (1 << BTN_MIDDLE) != 0 } else { c.active == id && c.mbtn & (1 << BTN_LEFT) != 0 };
774                    if held {
775                        if mx != x0 || my != y0 {
776                            st.fit = false;
777                            st.cx = cx - (mx - x0) as f64 / s;
778                            st.cy = cy - (my - y0) as f64 / s;
779                        }
780                    } else {
781                        st.g = Gesture::Idle;
782                    }
783                }
784                Gesture::Rect { x, y } => {
785                    if !(c.active == id && c.mbtn & (1 << BTN_LEFT) != 0) {
786                        st.g = Gesture::Idle;
787                        let lim = self.dp(4);
788                        if (mx - x).abs() > lim && (my - y).abs() > lim {
789                            // view px -> image px
790                            let ox = cxv - st.cx * s;
791                            let oy = cyv - st.cy * s;
792                            let (x0, x1) = (x.min(mx) as f64, x.max(mx) as f64);
793                            let (y0, y1) = (y.min(my) as f64, y.max(my) as f64);
794                            st.zoom_to([(x0 - ox) / s, (y0 - oy) / s, (x1 - x0) / s, (y1 - y0) / s]);
795                        }
796                    }
797                }
798                Gesture::Idle => {}
799            }
800            // a double click: 100 % at the pointer, else fit
801            if ev & EV_DOUBLE != 0 {
802                st.g = Gesture::Idle;
803                if st.fit || (st.zoom - 1.0).abs() > 1e-9 {
804                    st.zoom_at(1.0, mx as f64 + 0.5 - cxv, my as f64 + 0.5 - cyv);
805                } else {
806                    st.fit = true;
807                }
808            }
809            // keys
810            if self.wp.ctx.focus == id {
811                let s = st.zoom * unit;
812                let (px, py) = (vw / 8.0 / s, vh / 8.0 / s);
813                let mut moved = false;
814                for (vk, ddx, ddy) in [(VK_LEFT, -px, 0.0), (VK_RIGHT, px, 0.0), (VK_UP, 0.0, -py), (VK_DOWN, 0.0, py)] {
815                    if self.iv_key(vk, 0, MOD_CTRL | MOD_ALT) {
816                        st.cx += ddx;
817                        st.cy += ddy;
818                        moved = true;
819                    }
820                }
821                let page = vh * 0.9 / s;
822                if self.iv_key(VK_PRIOR, 0, MOD_CTRL | MOD_ALT) {
823                    st.cy -= page;
824                    moved = true;
825                }
826                if self.iv_key(VK_NEXT, 0, MOD_CTRL | MOD_ALT) {
827                    st.cy += page;
828                    moved = true;
829                }
830                if self.iv_key(VK_HOME, 0, MOD_ALT) {
831                    st.cy = f64::MIN / 4.0;
832                    moved = true;
833                }
834                if self.iv_key(VK_END, 0, MOD_ALT) {
835                    st.cy = f64::MAX / 4.0;
836                    moved = true;
837                }
838                if moved {
839                    st.fit = false;
840                    self.wp.ctx.focus_visible = true;
841                }
842                for (vk, dir) in [(VK_OEM_PLUS, 1), (VK_ADD, 1), (VK_OEM_MINUS, -1), (VK_SUBTRACT, -1)] {
843                    if self.iv_key(vk, 0, MOD_ALT) {
844                        st.step(dir);
845                    }
846                }
847                if self.iv_key(VK_0, MOD_CTRL, MOD_CTRL | MOD_ALT) || self.iv_key(VK_NUMPAD0, MOD_CTRL, MOD_CTRL | MOD_ALT) {
848                    st.fit = true;
849                }
850                if self.iv_key(VK_1, MOD_CTRL, MOD_CTRL | MOD_ALT) || self.iv_key(VK_NUMPAD0 + 1, MOD_CTRL, MOD_CTRL | MOD_ALT) {
851                    st.set_actual();
852                }
853            }
854        }
855        if st.fit {
856            st.zoom = fitz;
857            st.cx = iw * 0.5;
858            st.cy = ih * 0.5;
859        }
860        // the centre inside the image (or the image centred when it fits)
861        let s = st.zoom * unit;
862        let clampc = |c: f64, len: f64, v: f64| -> f64 {
863            let half = v * 0.5 / s;
864            if len * s <= v { len * 0.5 } else { c.clamp(half, len - half) }
865        };
866        if has && s > 0.0 {
867            st.cx = clampc(st.cx, iw, vw);
868            st.cy = clampc(st.cy, ih, vh);
869        }
870        // ---- drawing
871        let ox = (cxv - st.cx * s).rnd();
872        let oy = (cyv - st.cy * s).rnd();
873        if let ViewImage::Bitmap(sf, _) = img {
874            // the mip levels when the image is shown below 50 %
875            let base: &Surface = st.conv.as_deref().unwrap_or(sf);
876            if s < 0.5 && st.mips.is_none() && base.w > 1 && base.h > 1 {
877                if let Some(mut m) = MipChain::alloc(base.w, base.h, 0) {
878                    if m.update(base, None) == 0 {
879                        st.mips = Some(Rc::new(m));
880                    }
881                }
882            }
883        }
884        let dark = self.theme_dark;
885        let hc = self.high_contrast();
886        let bg = self.color(if hc { T_BG } else { T_BG_ALT });
887        let checker = if flags & IV_NO_CHECKER != 0 || hc {
888            None
889        } else if dark {
890            Some((self.dp(8), 0x2B2B2B, 0x3A3A3A))
891        } else {
892            Some((self.dp(8), 0xFFFFFF, 0xE3E3E3))
893        };
894        let grid = if hc {
895            self.color(T_TEXT) & 0xFF_FFFF | 0x9000_0000
896        } else if dark {
897            0x60FF_FFFF
898        } else {
899            0x5000_0000
900        };
901        let color = if flags & IV_TEXT_COLOR != 0 { self.color(T_TEXT) } else { 0xFF00_0000 };
902        let fonts = self.svg_fonts();
903        // what the view shows, wherever it lies
904        let mut h = Fnv::new();
905        h.u64(key as u64 | (st.ver as u64) << 48);
906        h.u64(flags as u64 | (bg as u64) << 32);
907        h.u64(checker.map_or(0, |c| c.0 as u64 | (c.1 as u64) << 16 | (c.2 as u64) << 40));
908        h.u64(grid as u64 | (color as u64) << 32);
909        h.u64(Rc::as_ptr(&fonts) as usize as u64 ^ st.mips.as_ref().map_or(0, |m| Rc::as_ptr(m) as usize as u64));
910        let content = h.0;
911        h.u64(s.to_bits());
912        h.u64(ox.to_bits());
913        h.u64(oy.to_bits());
914        let hash = h.0;
915        let obj = match &st.draw {
916            Some((k, d)) if *k == hash => d.clone(),
917            _ => {
918                let image = match img {
919                    ViewImage::Bitmap(sf, _) => IvImg::Bitmap(st.conv.clone().unwrap_or_else(|| sf.clone())),
920                    ViewImage::Svg(d) => IvImg::Svg(d.clone()),
921                };
922                let d = Rc::new(IvDraw {
923                    img: image,
924                    mips: st.mips.clone(),
925                    s,
926                    ox,
927                    oy,
928                    iw,
929                    ih,
930                    flags,
931                    bg,
932                    checker,
933                    grid,
934                    color,
935                    fonts,
936                });
937                st.draw = Some((hash, d.clone()));
938                d
939            }
940        };
941        self.ltr_begin(r[0], r[2]);
942        self.clip_push(r[0], r[1], r[2], r[3]);
943        self.cmd_custom(r[0], r[1], r[2], r[3], Some(obj.clone()), 0, hash as u32);
944        if has {
945            let (view, ver, gpu) = (CanvasView { scale: s, ox, oy }, (content ^ content >> 32) as u32, st.gpu);
946            self.canvas_gpu(CanvasDesc { id, name: "image", rect: r, view, ver, content: obj, gpu, exact_pan: false });
947        }
948        // scroll bars (over the image; their hit boxes after the view's)
949        if has && flags & IV_NO_BARS == 0 {
950            let sty = self.style;
951            let t = sty.scrollbar_thickness(self);
952            let (cw, ch) = ((iw * s).ceil_i64().min(i32::MAX as i64 / 2) as i32, (ih * s).ceil_i64().min(i32::MAX as i64 / 2) as i32);
953            let (bw, bh) = (r[2], r[3]);
954            let vneed = ch > bh;
955            let hneed = cw > bw;
956            if vneed {
957                let mut pos = (st.cy * s - vh * 0.5).rnd() as i32;
958                self.next_rect_px(r[0] + r[2] - t, r[1], t, if hneed { bh - t } else { bh });
959                if self.scrollbar(part(id, 1), &mut pos, ch, bh, true) & EV_ACT != 0 {
960                    st.fit = false;
961                    st.cy = (pos as f64 + vh * 0.5) / s;
962                }
963            }
964            if hneed {
965                let mut pos = (st.cx * s - vw * 0.5).rnd() as i32;
966                self.next_rect_px(r[0], r[1] + r[3] - t, if vneed { bw - t } else { bw }, t);
967                if self.scrollbar(part(id, 2), &mut pos, cw, bw, false) & EV_ACT != 0 {
968                    st.fit = false;
969                    st.cx = (pos as f64 + vw * 0.5) / s;
970                }
971            }
972        }
973        // the zoom rectangle
974        if let Gesture::Rect { x, y } = st.g {
975            let (x0, y0) = (x.min(mx), y.min(my));
976            let (w, hh) = ((x - mx).abs(), (y - my).abs());
977            let acc = self.color(T_ACCENT);
978            self.draw_color(acc & 0xFF_FFFF | 0x3000_0000);
979            self.draw_rect(x0, y0, w, hh);
980            self.draw_color(acc);
981            self.draw_rect(x0, y0, w, 1);
982            self.draw_rect(x0, y0 + hh - 1, w, 1);
983            self.draw_rect(x0, y0, 1, hh);
984            self.draw_rect(x0 + w - 1, y0, 1, hh);
985        }
986        self.clip_pop();
987        self.ltr_end();
988        // focus ring, the record
989        self.wp.ctx.last_rect = r;
990        self.wp.ctx.last_state = state;
991        self.focus_ring_draw(ev);
992        let label = if !st.label.is_empty() {
993            st.label.clone()
994        } else {
995            match img {
996                ViewImage::Svg(d) if !d.title().is_empty() => String::from(d.title()),
997                _ => String::from(tr("Bild")),
998            }
999        };
1000        self.wp.ctx.last_rect = r;
1001        self.wp.ctx.last_state = state;
1002        let changed = (st.zoom, st.cx, st.cy, st.fit) != view0;
1003        if changed {
1004            self.repass();
1005        }
1006        self.sem(K_IMAGE, id, Some(&label), st.zoom_pct());
1007        if changed { ev | EV_ACT } else { ev }
1008    }
1009}