refactor(editor): re-export painter contract from jian-widgets
This commit is contained in:
parent
0daca70f74
commit
42de84c917
9
Cargo.lock
generated
9
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Color>` 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<TextRun>,
|
||||
/// 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,
|
||||
};
|
||||
|
|
|
|||
2
vendor/jian
vendored
2
vendor/jian
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit cd408ca9e5897106c177f3dc0feaedb68ab71aff
|
||||
Subproject commit ebfbef776e13586d48234d01d1680cedee49106f
|
||||
Loading…
Reference in a new issue