diff --git a/Cargo.lock b/Cargo.lock index e070af693..b1e965677 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2150,6 +2150,14 @@ dependencies = [ "skia-safe", ] +[[package]] +name = "jian-widgets" +version = "0.0.1" +dependencies = [ + "glam", + "jian-core", +] + [[package]] name = "jni" version = "0.22.4" @@ -3020,6 +3028,7 @@ dependencies = [ "glam", "jian-core", "jian-ops-schema", + "jian-widgets", "op-ai", "op-i18n", "serde_json", diff --git a/crates/op-editor-core/Cargo.toml b/crates/op-editor-core/Cargo.toml index d2cd38911..e1f802365 100644 --- a/crates/op-editor-core/Cargo.toml +++ b/crates/op-editor-core/Cargo.toml @@ -9,6 +9,7 @@ description = "OpenPencil editor state — selection/tool/history/viewport on ji [dependencies] jian-ops-schema = { path = "../../vendor/jian/crates/jian-ops-schema" } jian-core = { path = "../../vendor/jian/crates/jian-core" } +jian-widgets = { path = "../../vendor/jian/crates/jian-widgets" } # Locale enum for the chrome UI state (ui_chrome.rs). op-i18n is # dependency-free + wasm32-clean, so it doesn't compromise the diff --git a/crates/op-editor-core/src/render_backend.rs b/crates/op-editor-core/src/render_backend.rs index a7ccbe8b5..0c9e0e379 100644 --- a/crates/op-editor-core/src/render_backend.rs +++ b/crates/op-editor-core/src/render_backend.rs @@ -1,687 +1,11 @@ -//! OP `RenderBackend` widget-facing facade (spec v19 §5.2). +//! Compatibility re-exports for the widget painter facade. //! -//! This is OP's design contract (method-style API: `fill_rect / stroke_rect / -//! draw_text / clip_rect / save / restore / translate / resize / dpi_scale`), -//! which does not line up directly with Jian's -//! `jian_core::render::RenderBackend` (command-buffer style: `new_surface / -//! begin_frame / draw(&DrawOp)` etc.). -//! -//! Implementation paths (per §5.2.1): -//! - `NativeBackend` (shell-native, frame-scoped design from Step 1a onward) -//! does **not** impl this trait directly; instead it exposes same-named -//! methods that take an explicit `canvas: &skia_safe::Canvas` argument. -//! The Step 1a basic_window demo invokes NativeBackend methods inside a -//! `SharedSkiaContext::with_frame` closure. Once Step 1c+ wires in a real -//! widget tree we can revisit a `WithCanvas<'a>` newtype to inject the -//! canvas into a trait impl. -//! - `WebCanvasKitBackend` (shell-web, Step 1b): backed by CanvasKit JS bindings. -//! - `MobileBackend` (Step 1f): backed by Metal / Vulkan / OpenGL ES. -//! -//! This module does **not** pull in skia-safe / Canvas / GL types — shell-core -//! must stay wasm32-clean (per spec §1.2 boundary). +//! The implementation moved to `jian-widgets` as part of the +//! cross-platform component-library extraction. Existing OP code keeps +//! importing this module during migration; new shared widget code should +//! import from `jian_widgets` directly. -use jian_core::render::{TextAlign, TextRun}; - -/// 2D coordinate point (spec §5.2 fixes this to `glam::Vec2`). -pub type Point2D = glam::Vec2; - -/// Rectangle (origin + size as two Vec2s). -/// -/// Derives `PartialEq` so `widgets::LayoutBox` can compare layout -/// results in tests. `Eq` is intentionally NOT derived: `Vec2` carries -/// floats, and exact float equality is only meaningful when callers -/// have been careful about how the values were produced. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Rect { - pub origin: Point2D, - pub size: Point2D, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn contains_is_closed_interval() { - let r = Rect::xywh(10.0, 20.0, 100.0, 50.0); - assert!(r.contains(Point2D::new(10.0, 20.0))); - assert!(r.contains(Point2D::new(110.0, 70.0))); - assert!(!r.contains(Point2D::new(9.99, 20.0))); - assert!(!r.contains(Point2D::new(10.0, 70.01))); - } - - #[test] - fn with_alpha_overrides_only_alpha() { - let c = Color::RED.with_alpha(0.5); - assert_eq!((c.r, c.g, c.b, c.a), (1.0, 0.0, 0.0, 0.5)); - } - - #[test] - fn to_jian_round_trips_named_colors() { - let j = Color::WHITE.to_jian(); - assert_eq!(j, jian_core::scene::Color::rgba(255, 255, 255, 255)); - } -} - -impl Rect { - /// Zero-sized rect at the origin. Used by Document::Node as the - /// "no bounds set" / container-only sentinel, and by tests - /// constructing throwaway rects. - pub const ZERO: Rect = Rect { - origin: Point2D::new(0.0, 0.0), - size: Point2D::new(0.0, 0.0), - }; - - /// Convenience builder: `Rect::xywh(x, y, w, h)`. - pub const fn xywh(x: f32, y: f32, width: f32, height: f32) -> Self { - Rect { - origin: Point2D::new(x, y), - size: Point2D::new(width, height), - } - } - - /// Closed-interval point hit-test. Replaces the hand-rolled - /// free-function `rect_contains` copies across the workspace. - pub fn contains(&self, p: Point2D) -> bool { - p.x >= self.origin.x - && p.x <= self.origin.x + self.size.x - && p.y >= self.origin.y - && p.y <= self.origin.y + self.size.y - } -} - -/// RGBA color (widget facade layer; all components 0.0..=1.0). -/// -/// Named constants live at the OP layer (spec v19 round 5 CONCERN-R5-3 fix); -/// `jian_core::scene::Color` is `Color(pub u32)` packed RGBA without RED/BLACK/etc -/// named constants — callers must construct it explicitly via `JianColor::rgb(...)`. -// PartialEq + no Eq: f32 components mean exact equality is rare, -// but Step 3's `Document::Node::fill: Option` and -// `Stroke { color: Color }` need PartialEq for derived comparisons -// in test assertions. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Color { - pub r: f32, - pub g: f32, - pub b: f32, - pub a: f32, -} - -impl Color { - pub const RED: Self = Self { - r: 1.0, - g: 0.0, - b: 0.0, - a: 1.0, - }; - pub const GREEN: Self = Self { - r: 0.0, - g: 1.0, - b: 0.0, - a: 1.0, - }; - pub const BLUE: Self = Self { - r: 0.0, - g: 0.0, - b: 1.0, - a: 1.0, - }; - pub const BLACK: Self = Self { - r: 0.0, - g: 0.0, - b: 0.0, - a: 1.0, - }; - pub const WHITE: Self = Self { - r: 1.0, - g: 1.0, - b: 1.0, - a: 1.0, - }; - pub const TRANSPARENT: Self = Self { - r: 0.0, - g: 0.0, - b: 0.0, - a: 0.0, - }; - - pub const fn with_alpha(self, a: f32) -> Self { - Self { a, ..self } - } - - pub fn to_jian(self) -> jian_core::scene::Color { - fn ch(v: f32) -> u8 { - (v.clamp(0.0, 1.0) * 255.0).round() as u8 - } - jian_core::scene::Color::rgba(ch(self.r), ch(self.g), ch(self.b), ch(self.a)) - } -} - -/// OP's TextLayout — defined at the OP layer after v19 removed parley; a thin -/// wrapper over `jian_core::render::TextRun`. -/// -/// It does not hold a layout context or glyph cache (shell-core is wasm32-clean -/// and cannot pull in skia/parley/icu); the real layout happens inside -/// NativeBackend::draw_text via `jian_skia::SkiaBackend`'s textlayout feature -/// (skia textlayout, ICU+harfbuzz) for shaping + line breaking. -/// -/// In Step 1a, TextLayout is a placeholder for "a collection of already-shaped -/// TextRuns"; caret/selection/bidi/wrap are deferred to Step 1c+. The -/// signatures are pinned to avoid breaking the API in later steps. -#[derive(Debug, Clone)] -pub struct TextLayout { - runs: Vec, - /// Whether every run renders with an italic (slanted) face. - /// Carried at the OP layer because `jian_core::render::TextRun` - /// has no slant field; backends read it via [`TextLayout::italic`] - /// when resolving typefaces. - italic: bool, -} - -/// Raster image placement mode. Matches the canonical image-fill / -/// image-node modes used by the TS renderer. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum ImageDrawMode { - #[default] - Fill, - Fit, - Crop, - Tile, - Stretch, -} - -/// Per-image adjustment values from the image-fill popover. Each -/// value is in the TS slider range `[-100, 100]`. -#[derive(Debug, Clone, Copy, PartialEq, Default)] -pub struct ImageAdjustments { - pub exposure: f32, - pub contrast: f32, - pub saturation: f32, - pub temperature: f32, - pub tint: f32, - pub highlights: f32, - pub shadows: f32, -} - -impl ImageAdjustments { - pub fn is_neutral(self) -> bool { - self.exposure == 0.0 - && self.contrast == 0.0 - && self.saturation == 0.0 - && self.temperature == 0.0 - && self.tint == 0.0 - && self.highlights == 0.0 - && self.shadows == 0.0 - } -} - -impl TextLayout { - /// Single-run constructor (the only active path in Step 1a; used by the - /// Phase B/C demo + raster_text_smoke). - /// - /// Fields align with `jian_core::render::TextRun` - /// (`vendor/jian/crates/jian-core/src/render/paint.rs:77-94`): - /// **TextRun has no `Default` impl**, so every field must be set explicitly. - /// - `content` / `font_family` / `font_size` / `color` / `origin`: passed in by caller - /// - `font_weight: 400` (CSS Normal) - /// - `max_width: 0.0` ("unknown; render at origin with no alignment adjustment") - /// - `align: TextAlign::Start` - /// - `line_height: 0.0` ("default") - pub fn single_run( - content: &str, - font_family: &str, - font_size: f32, - color: jian_core::scene::Color, - origin: Point2D, - ) -> Self { - let run = TextRun { - content: content.to_string(), - font_family: font_family.to_string(), - font_size, - font_weight: 400, - color, - origin: jian_core::geometry::Point::new(origin.x, origin.y), - max_width: 0.0, - align: TextAlign::Start, - line_height: 0.0, - }; - Self { - runs: vec![run], - italic: false, - } - } - - /// View of the already-shaped TextRun collection. - pub fn runs(&self) -> &[TextRun] { - &self.runs - } - - /// Overwrite every run's CSS-style numeric weight. The convenience - /// constructor hardcodes 400; canvas-side text from `.op` files - /// honours `Node.font_weight` via this builder. - pub fn with_font_weight(mut self, weight: u16) -> Self { - for r in &mut self.runs { - r.font_weight = weight; - } - self - } - - /// Request an italic (slanted) face for every run. Backends with - /// a real font manager resolve a slanted typeface; single-face - /// backends synthesise an oblique via glyph skew. - pub fn with_italic(mut self, italic: bool) -> Self { - self.italic = italic; - self - } - - /// Whether the layout renders italic. See [`TextLayout::with_italic`]. - pub fn italic(&self) -> bool { - self.italic - } - - /// Translates every run's origin (NativeBackend::draw_text adds this on top - /// of the widget origin). Returns a new layout; the original is unchanged. - pub fn translated(&self, offset: Point2D) -> Self { - let runs = self - .runs - .iter() - .map(|r| { - let mut r2 = r.clone(); - r2.origin = - jian_core::geometry::Point::new(r.origin.x + offset.x, r.origin.y + offset.y); - r2 - }) - .collect(); - Self { - runs, - italic: self.italic, - } - } -} - -/// Backend abstraction (widget-facing facade, spec §5.2). -/// -/// Note: the trait has no `Send` bound — skia-safe types are `!Send` -/// (rust-skia is thread-bound), and the Backend is used solely on the render -/// thread, so cross-thread access is unnecessary. -/// -/// In 1a, `NativeBackend` (shell-native) does **not** impl this trait directly -/// (v19 round 3 BLOCK-R3-3 fix — the frame-scoped design avoids cross-frame -/// borrows); instead it exposes the same-named methods with an explicit -/// `canvas: &skia_safe::Canvas` argument. The trait signature contains no -/// Skia / GPU-backend-specific types. -/// Pre-multiply a colour's alpha by a 0..=1 opacity multiplier — -/// shared by the gradient default impls when they degrade to a solid -/// first-stop fill. -fn fold_alpha(c: Color, opacity: f32) -> Color { - Color { - a: c.a * opacity.clamp(0.0, 1.0), - ..c - } -} - -pub trait RenderBackend { - /// Begin a frame; the backend tracks current frame state internally - /// (canvas type is not exposed). - fn begin_frame(&mut self); - fn end_frame(&mut self); - - // Drawing primitives — widgets call these and never touch the canvas directly. - fn fill_rect(&mut self, rect: Rect, color: Color); - fn stroke_rect(&mut self, rect: Rect, color: Color, width: f32); - fn draw_text(&mut self, layout: &TextLayout, origin: Point2D); - fn clip_rect(&mut self, rect: Rect); - - /// Intersect the clip with a rounded rectangle — used by `clipContent` - /// containers so children clip to the frame's rounded corners. - /// Default degrades to the sharp [`RenderBackend::clip_rect`] so - /// backends without an rrect-clip primitive still constrain - /// children to the box (corners just clip square). - fn clip_round_rect(&mut self, rect: Rect, radius: f32) { - let _ = radius; - self.clip_rect(rect); - } - - // Step 4 visual lift — line + rounded rect primitives so widgets can - // draw lucide-style icons and shadcn-style chip / panel / button - // backgrounds without dropping back to per-backend skia calls. - /// Stroke a single line segment from `from` to `to`. Used for - /// icon line art (cursor arrow, T glyph, hash strokes, etc.). - fn stroke_line(&mut self, from: Point2D, to: Point2D, color: Color, width: f32); - /// Filled rectangle with corner radius. `radius` is uniform on - /// all four corners. - fn fill_round_rect(&mut self, rect: Rect, radius: f32, color: Color); - /// Stroked rounded rectangle. Mirrors `stroke_rect` + `radius`. - fn stroke_round_rect(&mut self, rect: Rect, radius: f32, color: Color, width: f32); - /// Step 5 SVG icons: stroke an SVG path `d` string scaled from - /// a 24×24 viewBox into a `size × size` square anchored at - /// `top_left`. Backends parse the d-string via skia's path - /// parser and render with round caps + joins to match - /// lucide's visual style. Used by the icon module so widgets - /// just declare which lucide path to draw. - fn stroke_svg_path(&mut self, d: &str, top_left: Point2D, size: f32, color: Color, width: f32); - - /// Fill an SVG path scaled from `viewbox` × `viewbox` to - /// `size × size` anchored at `top_left`. Used for brand - /// logos whose source paths are designed as filled shapes - /// (Claude, OpenAI, etc.) rather than lucide's stroked - /// 24×24 grid. Default-impl is a no-op so backends without - /// a path-fill primitive degrade gracefully. - fn fill_svg_path( - &mut self, - _d: &str, - _top_left: Point2D, - _size: f32, - _viewbox: f32, - _color: Color, - ) { - } - - /// Fill an arbitrary SVG path fitted into `rect` by the path's - /// own tight bounds. Document Path nodes use this instead of the - /// icon-oriented square viewBox helper because imported Figma - /// vectors often carry negative local coordinates. - fn fill_svg_path_in_rect(&mut self, d: &str, rect: Rect, color: Color) { - self.fill_svg_path(d, rect.origin, 1.0, 1.0, color); - } - - /// Stroke an arbitrary SVG path fitted into `rect` by the path's - /// own tight bounds. Default degrades to the icon-oriented path - /// helper for simple backends. - fn stroke_svg_path_in_rect(&mut self, d: &str, rect: Rect, color: Color, width: f32) { - self.stroke_svg_path(d, rect.origin, rect.size.x.max(rect.size.y), color, width); - } - - /// Fill an arbitrary SVG path (fitted into `rect` by its own tight - /// bounds) with a linear gradient. `stops` / `angle_deg` / - /// `opacity` follow the same convention as - /// [`fill_round_rect_linear_gradient`]. Default degrades to a - /// solid first-stop fill so backends without a path-gradient - /// shader still paint something resembling the gradient. - fn fill_svg_path_in_rect_linear_gradient( - &mut self, - d: &str, - rect: Rect, - stops: &[(f32, Color)], - _angle_deg: f32, - opacity: f32, - ) { - if let Some((_, c)) = stops.first() { - self.fill_svg_path_in_rect(d, rect, fold_alpha(*c, opacity)); - } - } - - /// Paint an inset (inner) shadow for an SVG path fitted into - /// `rect`: a blurred shadow that bleeds inward from the path - /// silhouette, offset by `(offset_x, offset_y)`. Default is a - /// no-op — backends without the primitive simply omit the inset - /// shadow rather than drawing it as an outer one. - #[allow(clippy::too_many_arguments)] - fn fill_inner_shadow_svg_path( - &mut self, - _d: &str, - _rect: Rect, - _offset_x: f32, - _offset_y: f32, - _blur: f32, - _color: Color, - ) { - } - - /// Fill an arbitrary SVG path (fitted into `rect`) with a radial - /// gradient. Centre / radius fractions follow - /// [`fill_round_rect_radial_gradient`]. Default degrades to a - /// solid first-stop fill. - #[allow(clippy::too_many_arguments)] - fn fill_svg_path_in_rect_radial_gradient( - &mut self, - d: &str, - rect: Rect, - stops: &[(f32, Color)], - _cx_frac: f32, - _cy_frac: f32, - _radius_frac: f32, - opacity: f32, - ) { - if let Some((_, c)) = stops.first() { - self.fill_svg_path_in_rect(d, rect, fold_alpha(*c, opacity)); - } - } - - /// Paint a drop shadow: a gaussian-blurred filled rounded - /// rectangle. `rect` is the already-offset shadow bounds, - /// `radius` the corner radius (0 for sharp corners, min-half - /// for an ellipse-shaped shadow), `blur` the gaussian sigma in - /// device px (caller applies zoom). Painted BEHIND the node's - /// own fill. Default impl approximates with an un-blurred - /// translucent fill so non-blur backends still hint at depth; - /// native + web override with skia's `MaskFilter::blur`. - fn fill_drop_shadow(&mut self, rect: Rect, radius: f32, blur: f32, color: Color) { - let _ = blur; - self.fill_round_rect(rect, radius, color); - } - - /// Filled ellipse inscribed in `bounds`. Used by Ellipse-kind - /// document nodes. Default impl falls back to a rounded - /// rectangle (visually wrong but compiles); native + web - /// override with `Canvas::draw_oval`. - fn fill_oval(&mut self, bounds: Rect, color: Color) { - let radius = bounds.size.x.min(bounds.size.y) / 2.0; - self.fill_round_rect(bounds, radius, color); - } - - /// Stroked ellipse inscribed in `bounds`. Pairs with - /// `fill_oval`. Default-impl behaves like `stroke_round_rect` - /// at max corner radius. - fn stroke_oval(&mut self, bounds: Rect, color: Color, width: f32) { - let radius = bounds.size.x.min(bounds.size.y) / 2.0; - self.stroke_round_rect(bounds, radius, color, width); - } - - /// Fill a batch of identical round dots — `centers` are the dot - /// centres, each painted as a filled circle of `radius`. The - /// canvas grid calls this once per frame instead of emitting one - /// draw op per dot (~1000+ on a full viewport), which is the - /// dominant cost of an empty-canvas pan / drag. The default impl - /// loops `fill_oval` so any backend stays correct; native - /// overrides with a single batched `Canvas::draw_points`. - fn fill_dots(&mut self, centers: &[Point2D], radius: f32, color: Color) { - for c in centers { - self.fill_oval( - Rect { - origin: Point2D::new(c.x - radius, c.y - radius), - size: Point2D::new(radius * 2.0, radius * 2.0), - }, - color, - ); - } - } - - /// Fill a closed polygon outlined by `points` (≥ 3 vertices). - /// Default impl emits a `stroke_svg_path` fill — works for - /// triangles + small polygons; backends with a richer canvas - /// override. - fn fill_polygon(&mut self, _points: &[Point2D], _color: Color) { - // Default no-op so non-overriding backends just skip the - // fill. Native + web override. - } - - /// Stroke a closed polygon outlined by `points`. Default impl - /// walks the points and emits N `stroke_line` calls. - fn stroke_polygon(&mut self, points: &[Point2D], color: Color, width: f32) { - if points.len() < 2 { - return; - } - for i in 0..points.len() { - let a = points[i]; - let b = points[(i + 1) % points.len()]; - self.stroke_line(a, b, color, width); - } - } - - /// Draw a raster image, aspect-fit + centered inside `rect`. - /// `image_id` is a process-stable handle the backend keys its - /// decode cache on; `encoded` is the raw image bytes (PNG / - /// JPEG / …), consulted only on the first (cache-miss) draw of - /// that id. Default impl is a no-op — backends without an image - /// pipeline (or codec support) degrade gracefully and the caller - /// is expected to have painted a placeholder frame underneath. - fn draw_image(&mut self, _rect: Rect, _image_id: u64, _encoded: &[u8]) {} - - /// Draw a raster image using an explicit fill/fit/crop/tile mode. - /// Backends without mode-aware support fall back to `draw_image`. - fn draw_image_with_mode( - &mut self, - rect: Rect, - image_id: u64, - encoded: &[u8], - mode: ImageDrawMode, - ) { - let _ = mode; - self.draw_image(rect, image_id, encoded); - } - - /// Draw a raster image with both placement and image-adjustment - /// controls. Backends without color-filter support fall back to - /// mode-aware drawing. - fn draw_image_with_options( - &mut self, - rect: Rect, - image_id: u64, - encoded: &[u8], - mode: ImageDrawMode, - adjustments: ImageAdjustments, - opacity: f32, - corner_radius: f32, - ) { - let _ = (adjustments, opacity, corner_radius); - self.draw_image_with_mode(rect, image_id, encoded, mode); - } - - /// Fill a (round-)rectangle with a linear gradient. `stops` - /// carries `(offset, color)` pairs in 0.0..=1.0 with at least one - /// entry; `angle_deg` is the gradient direction in the canonical - /// `.op` convention (0° = bottom→top, 90° = left→right, matches - /// CSS `to-top`); `opacity` is the per-fill multiplier folded - /// into every stop's alpha. A `radius` of 0 paints a plain rect. - /// - /// Default impl degrades to a solid `fill_round_rect` using the - /// first stop's colour pre-multiplied by `opacity` so backends - /// without a gradient shader still paint something resembling - /// the gradient. - fn fill_round_rect_linear_gradient( - &mut self, - rect: Rect, - radius: f32, - stops: &[(f32, Color)], - _angle_deg: f32, - opacity: f32, - ) { - if let Some((_, c)) = stops.first() { - let color = Color { - r: c.r, - g: c.g, - b: c.b, - a: c.a * opacity.clamp(0.0, 1.0), - }; - self.fill_round_rect(rect, radius, color); - } - } - - /// Fill a (round-)rectangle with a radial gradient. `cx_frac` - /// / `cy_frac` are 0.0..=1.0 fractions of `rect`'s width / height - /// for the centre; `radius_frac` is a 0.0..=1.0 fraction of - /// `max(w, h)` for the outer radius — matches the TS - /// `pen-renderer` so the same `.op` file paints at the same - /// radial size on native + web + export. Stops + opacity follow - /// the same convention as the linear variant. - /// - /// Default impl falls back to a solid first-stop fill, same as - /// the linear variant. - #[allow(clippy::too_many_arguments)] - fn fill_round_rect_radial_gradient( - &mut self, - rect: Rect, - radius: f32, - stops: &[(f32, Color)], - _cx_frac: f32, - _cy_frac: f32, - _radius_frac: f32, - opacity: f32, - ) { - if let Some((_, c)) = stops.first() { - let color = Color { - r: c.r, - g: c.g, - b: c.b, - a: c.a * opacity.clamp(0.0, 1.0), - }; - self.fill_round_rect(rect, radius, color); - } - } - - // Transform stack. - fn save(&mut self); - fn restore(&mut self); - fn translate(&mut self, offset: Point2D); - /// Scale the current transform around `pivot`. Used for node - /// `flipX` / `flipY` parity with the TS renderer. - fn scale(&mut self, _scale: Point2D, _pivot: Point2D) {} - /// Rotate the current transform `radians` clockwise about - /// `pivot` (in current-space coordinates, BEFORE the rotation - /// is applied). Default impl is a no-op so backends without - /// rotation support degrade gracefully; native + web override - /// using skia's `Canvas::rotate(angle, point)`. - fn rotate(&mut self, _radians: f32, _pivot: Point2D) {} - - // Viewport / DPI. - fn resize(&mut self, width: u32, height: u32); - fn dpi_scale(&self) -> f32; - - /// Measure the rendered horizontal advance of `text` at - /// `font_size`. Used by widgets that need precise centering - /// or right-alignment without round-trip to a layout pass. - /// Default impl: rough heuristic (~0.55 × font_size per - /// ASCII char, ~1.0 × font_size per non-ASCII char). Backends - /// that own a real typeface (Web + Native) override with - /// `Font::measure_str` for pixel-accurate positioning. - fn measure_text(&mut self, text: &str, font_size: f32) -> f32 { - self.measure_text_weighted(text, font_size, 400) - } - - /// Like `measure_text` but resolves the typeface using `weight` - /// (CSS-style 100-900). Backends that serve a weighted variant - /// can return wider/narrower advances; the wrap pass uses this - /// so line breaks decided pre-paint match what `draw_text` lays - /// down post-paint. Default impl forwards to the heuristic - /// `measure_text` so wasm builds without a real typeface stay - /// roughly correct. - fn measure_text_weighted(&mut self, text: &str, font_size: f32, _weight: u16) -> f32 { - let mut w = 0.0; - for c in text.chars() { - w += if c.is_ascii() { - font_size * 0.55 - } else { - font_size - }; - } - w - } - - /// Like `measure_text_weighted` but also slant-aware, so styled - /// text-run slices measure with the same (weight, italic) typeface - /// `draw_text` paints with. Default forwards to the weighted - /// measure — synthetic-oblique backends (glyph skew) keep upright - /// advances anyway; only backends that resolve a REAL italic face - /// need to override. - fn measure_text_styled( - &mut self, - text: &str, - font_size: f32, - weight: u16, - italic: bool, - ) -> f32 { - let _ = italic; - self.measure_text_weighted(text, font_size, weight) - } -} +pub use jian_widgets::geometry::{Color, Point2D, Rect}; +pub use jian_widgets::painter::{ + ImageAdjustments, ImageDrawMode, Painter as RenderBackend, TextLayout, +}; diff --git a/vendor/jian b/vendor/jian index cd408ca9e..ebfbef776 160000 --- a/vendor/jian +++ b/vendor/jian @@ -1 +1 @@ -Subproject commit cd408ca9e5897106c177f3dc0feaedb68ab71aff +Subproject commit ebfbef776e13586d48234d01d1680cedee49106f