diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f5ecefe..4e87fb86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,9 @@ env: RUST_MIN_VER: "1.70" # List of packages that will be checked with the minimum supported Rust version. # This should be limited to packages that are intended for publishing. - RUST_MIN_VER_PKGS: "-p parley -p fontique" + PRIMARY_PKGS: "-p parley -p fontique" + # List of packages that are not published on crates.io (examples, etc) + EXAMPLE_PKGS: "-p tiny_skia_render" # List of features that depend on the standard library and will be excluded from no_std checks. FEATURES_DEPENDING_ON_STD: "std,default,system" @@ -104,13 +106,16 @@ jobs: # TODO: Add --target x86_64-unknown-none to the no_std check once we solve the compilation issues with it - name: cargo clippy (no_std) - run: cargo hack clippy --workspace --locked --optional-deps --each-feature --features libm --exclude-features ${{ env.FEATURES_DEPENDING_ON_STD }} -- -D warnings + run: cargo hack clippy ${{ env.PRIMARY_PKGS }} --locked --optional-deps --each-feature --features libm --exclude-features ${{ env.FEATURES_DEPENDING_ON_STD }} -- -D warnings - name: cargo clippy - run: cargo hack clippy --workspace --locked --optional-deps --each-feature --features std -- -D warnings + run: cargo hack clippy ${{ env.PRIMARY_PKGS }} --locked --optional-deps --each-feature --features std -- -D warnings - name: cargo clippy (auxiliary) - run: cargo hack clippy --workspace --locked --optional-deps --each-feature --features std --tests --benches --examples -- -D warnings + run: cargo hack clippy ${{ env.PRIMARY_PKGS }} --locked --optional-deps --each-feature --features std --tests --benches --examples -- -D warnings + + - name: cargo clippy (examples) + run: cargo hack clippy ${{ env.EXAMPLE_PKGS }} --locked --optional-deps --each-feature -- -D warnings clippy-stable-android: name: cargo clippy (android) @@ -137,10 +142,13 @@ jobs: tool: cargo-hack - name: cargo clippy - run: cargo hack clippy --workspace --locked --target ${{ matrix.target }} --optional-deps --each-feature --features std -- -D warnings + run: cargo hack clippy ${{ env.PRIMARY_PKGS }} --locked --target ${{ matrix.target }} --optional-deps --each-feature --features std -- -D warnings - name: cargo clippy (auxiliary) - run: cargo hack clippy --workspace --locked --target ${{ matrix.target }} --optional-deps --each-feature --features std --tests --benches --examples -- -D warnings + run: cargo hack clippy ${{ env.PRIMARY_PKGS }} --locked --target ${{ matrix.target }} --optional-deps --each-feature --features std --tests --benches --examples -- -D warnings + + - name: cargo clippy (examples) + run: cargo hack clippy ${{ env.EXAMPLE_PKGS }} --locked --target ${{ matrix.target }} --optional-deps --each-feature -- -D warnings test-stable: name: cargo test @@ -160,7 +168,7 @@ jobs: toolchain: ${{ env.RUST_STABLE_VER }} - name: cargo test - run: cargo test --workspace --locked --all-features + run: cargo test ${{ env.PRIMARY_PKGS }} --locked --all-features check-msrv: name: cargo check (msrv) @@ -186,10 +194,10 @@ jobs: # TODO: Add --target x86_64-unknown-none to the no_std check once we solve the compilation issues with it - name: cargo check (no_std) - run: cargo hack check ${{ env.RUST_MIN_VER_PKGS }} --locked --optional-deps --each-feature --features libm --exclude-features ${{ env.FEATURES_DEPENDING_ON_STD }} + run: cargo hack check ${{ env.PRIMARY_PKGS }} --locked --optional-deps --each-feature --features libm --exclude-features ${{ env.FEATURES_DEPENDING_ON_STD }} - name: cargo check - run: cargo hack check ${{ env.RUST_MIN_VER_PKGS }} --locked --optional-deps --each-feature --features std + run: cargo hack check ${{ env.PRIMARY_PKGS }} --locked --optional-deps --each-feature --features std check-msrv-android: name: cargo check (msrv) (android) @@ -215,7 +223,7 @@ jobs: tool: cargo-hack - name: cargo check - run: cargo hack check ${{ env.RUST_MIN_VER_PKGS }} --locked --target ${{ matrix.target }} --optional-deps --each-feature --features std + run: cargo hack check ${{ env.PRIMARY_PKGS }} --locked --target ${{ matrix.target }} --optional-deps --each-feature --features std doc: name: cargo doc @@ -234,4 +242,4 @@ jobs: # We test documentation using nightly to match docs.rs. This prevents potential breakages - name: cargo doc - run: cargo doc --workspace --locked --all-features --no-deps --document-private-items -Zunstable-options -Zrustdoc-scrape-examples + run: cargo doc ${{ env.PRIMARY_PKGS }} --locked --all-features --no-deps --document-private-items -Zunstable-options -Zrustdoc-scrape-examples diff --git a/.gitignore b/.gitignore index ea8c4bf7..82469bec 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/examples/_output \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index a90ba5d5..03ba065c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "ahash" version = "0.8.11" @@ -20,6 +26,12 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + [[package]] name = "arrayvec" version = "0.7.4" @@ -34,9 +46,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bytemuck" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" +checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" dependencies = [ "bytemuck_derive", ] @@ -119,6 +131,15 @@ dependencies = [ "libm", ] +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "displaydoc" version = "0.2.4" @@ -144,6 +165,25 @@ dependencies = [ "wio", ] +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "font-types" version = "0.5.3" @@ -335,9 +375,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.154" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libm" @@ -351,6 +391,12 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d642685b028806386b2b6e75685faadd3eb65a85fff7df711ce18446a422da" +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + [[package]] name = "memmap2" version = "0.9.4" @@ -360,6 +406,16 @@ dependencies = [ "libc", ] +[[package]] +name = "miniz_oxide" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +dependencies = [ + "adler", + "simd-adler32", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -386,11 +442,24 @@ dependencies = [ "smallvec", ] +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" dependencies = [ "unicode-ident", ] @@ -422,24 +491,30 @@ checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" [[package]] name = "serde" -version = "1.0.199" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a" +checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.199" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc" +checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "skrifa" version = "0.19.1" @@ -463,11 +538,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + [[package]] name = "swash" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ec889a8e0a6fcb91041996c8f1f6be0fe1a09e94478785e07c32ce2bca2d2b" +checksum = "682a612b50baf09e8a039547ecf49e6c155690dcb751b1bcb19c93cdeb3d42d4" dependencies = [ "read-fonts", "yazi", @@ -476,9 +557,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.60" +version = "2.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106" dependencies = [ "proc-macro2", "quote", @@ -498,24 +579,60 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.59" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.59" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tiny_skia_render" +version = "0.1.0" +dependencies = [ + "parley", + "peniko", + "skrifa", + "tiny-skia", +] + [[package]] name = "tinystr" version = "0.7.5" @@ -619,18 +736,18 @@ checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697" [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 65ac3dd8..04b344d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,8 @@ [workspace] resolver = "2" members = [ - "fontique" + "fontique", + "examples/tiny_skia_render", ] [workspace.package] diff --git a/examples/tiny_skia_render/Cargo.toml b/examples/tiny_skia_render/Cargo.toml new file mode 100644 index 00000000..d538fbc8 --- /dev/null +++ b/examples/tiny_skia_render/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "tiny_skia_render" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +publish = false + +[dependencies] +parley = { path = "../.." } +skrifa = { workspace = true } +peniko = { workspace = true } +tiny-skia = "0.11" + +[lints] +workspace = true diff --git a/examples/tiny_skia_render/src/main.rs b/examples/tiny_skia_render/src/main.rs new file mode 100644 index 00000000..b40d83a6 --- /dev/null +++ b/examples/tiny_skia_render/src/main.rs @@ -0,0 +1,233 @@ +// Copyright 2024 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! A simple example that lays out some text using Parley, extracts outlines using Skrifa and +//! then paints those outlines using Tiny-Skia. +//! +//! Note: Emoji rendering is not currently implemented in this example. See the swash example +//! if you need emoji rendering. + +use std::path::PathBuf; + +use parley::layout::{Alignment, GlyphRun, Layout}; +use parley::style::{FontStack, FontWeight, StyleProperty}; +use parley::{FontContext, LayoutContext}; +use peniko::Color as PenikoColor; +use skrifa::instance::{LocationRef, NormalizedCoord, Size}; +use skrifa::outline::{DrawSettings, OutlinePen}; +use skrifa::raw::FontRef as ReadFontsRef; +use skrifa::{GlyphId, MetadataProvider, OutlineGlyph}; +use tiny_skia::{ + Color as TinySkiaColor, FillRule, Paint, PathBuilder, Pixmap, PixmapMut, Transform, +}; + +fn main() { + // The text we are going to style and lay out + let text = String::from( + "Some text here. Let's make it a bit longer so that line wrapping kicks in 😊. And also some اللغة العربية arabic text.", + ); + + // The display scale for HiDPI rendering + let display_scale = 1.0; + + // The width for line wrapping + let max_advance = Some(200.0 * display_scale); + + // Colours for rendering + let foreground_color = PenikoColor::rgb8(0, 0, 0); + let background_color = PenikoColor::rgb8(255, 255, 255); + + // Padding around the output image + let padding = 20; + + // Create a FontContext, LayoutContext + // + // These are both intended to be constructed rarely (perhaps even once per app (or once per thread)) + // and provide caches and scratch space to avoid allocations + let mut font_cx = FontContext::default(); + let mut layout_cx = LayoutContext::new(); + + // Create a RangedBuilder + let mut builder = layout_cx.ranged_builder(&mut font_cx, &text, display_scale); + + // Set default text colour styles (set foreground text color) + let brush_style = StyleProperty::Brush(foreground_color); + builder.push_default(&brush_style); + + // Set default font family + let font_stack = FontStack::Source("system-ui"); + let font_stack_style = StyleProperty::FontStack(font_stack); + builder.push_default(&font_stack_style); + builder.push_default(&StyleProperty::LineHeight(1.3)); + builder.push_default(&StyleProperty::FontSize(16.0)); + + // Set the first 4 characters to bold + let bold = FontWeight::new(600.0); + let bold_style = StyleProperty::FontWeight(bold); + builder.push(&bold_style, 0..4); + + // Build the builder into a Layout + let mut layout: Layout = builder.build(); + + // Perform layout (including bidi resolution and shaping) with start alignment + layout.break_all_lines(max_advance, Alignment::Start); + let width = layout.width().ceil() as u32; + let height = layout.height().ceil() as u32; + let padded_width = width + padding * 2; + let padded_height = height + padding * 2; + + // Create TinySkia Pixmap + let mut img = Pixmap::new(padded_width, padded_height).unwrap(); + + // Fill background color + img.fill(to_tiny_skia(background_color)); + + // Wrap Pixmap in a type that implements skrifa::OutlinePen + let mut pen = TinySkiaPen::new(img.as_mut()); + + // Render each glyph run + for line in layout.lines() { + for glyph_run in line.glyph_runs() { + render_glyph_run(&glyph_run, &mut pen, padding); + } + } + + // Write image to PNG file in examples/_output dir + let output_path: PathBuf = { + let path = std::path::PathBuf::from(file!()); + let mut path = std::fs::canonicalize(path).unwrap(); + path.pop(); + path.pop(); + path.pop(); + path.push("_output"); + let _ = std::fs::create_dir(path.clone()); + path.push("tiny_skia_render.png"); + path + }; + img.save_png(output_path).unwrap(); +} + +fn to_tiny_skia(color: PenikoColor) -> TinySkiaColor { + TinySkiaColor::from_rgba8(color.r, color.g, color.b, color.a) +} + +fn render_glyph_run(glyph_run: &GlyphRun, pen: &mut TinySkiaPen<'_>, padding: u32) { + // Resolve properties of the GlyphRun + let mut run_x = glyph_run.offset(); + let run_y = glyph_run.baseline(); + let style = glyph_run.style(); + let color = style.brush; + + // Get the "Run" from the "GlyphRun" + let run = glyph_run.run(); + + // Resolve properties of the Run + let font = run.font(); + let font_size = run.font_size(); + + let normalized_coords = run + .normalized_coords() + .iter() + .map(|coord| skrifa::instance::NormalizedCoord::from_bits(*coord)) + .collect::>(); + + // Get glyph outlines using Skrifa. This can be cached in production code. + let font_collection_ref = font.data.as_ref(); + let font_ref = ReadFontsRef::from_index(font_collection_ref, font.index).unwrap(); + let outlines = font_ref.outline_glyphs(); + + // Iterates over the glyphs in the GlyphRun + for glyph in glyph_run.glyphs() { + let glyph_x = run_x + glyph.x + padding as f32; + let glyph_y = run_y - glyph.y + padding as f32; + run_x += glyph.advance; + + let glyph_id = GlyphId::from(glyph.id); + let glyph_outline = outlines.get(glyph_id).unwrap(); + + pen.set_origin(glyph_x, glyph_y); + pen.set_color(to_tiny_skia(color)); + pen.draw_glyph(&glyph_outline, font_size, &normalized_coords); + } +} + +struct TinySkiaPen<'a> { + pixmap: PixmapMut<'a>, + x: f32, + y: f32, + paint: Paint<'static>, + open_path: PathBuilder, +} + +impl TinySkiaPen<'_> { + fn new(pixmap: PixmapMut) -> TinySkiaPen { + TinySkiaPen { + pixmap, + x: 0.0, + y: 0.0, + paint: Paint::default(), + open_path: PathBuilder::new(), + } + } + + fn set_origin(&mut self, x: f32, y: f32) { + self.x = x; + self.y = y; + } + + fn set_color(&mut self, color: TinySkiaColor) { + self.paint.set_color(color); + } + + fn draw_glyph( + &mut self, + glyph: &OutlineGlyph<'_>, + size: f32, + normalized_coords: &[NormalizedCoord], + ) { + let location_ref = LocationRef::new(normalized_coords); + let settings = DrawSettings::unhinted(Size::new(size), location_ref); + glyph.draw(settings, self).unwrap(); + + let builder = core::mem::replace(&mut self.open_path, PathBuilder::new()); + if let Some(path) = builder.finish() { + self.pixmap.fill_path( + &path, + &self.paint, + FillRule::Winding, + Transform::identity(), + None, + ); + } + } +} + +impl OutlinePen for TinySkiaPen<'_> { + fn move_to(&mut self, x: f32, y: f32) { + self.open_path.move_to(self.x + x, self.y - y); + } + + fn line_to(&mut self, x: f32, y: f32) { + self.open_path.line_to(self.x + x, self.y - y); + } + + fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) { + self.open_path + .quad_to(self.x + cx0, self.y - cy0, self.x + x, self.y - y); + } + + fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) { + self.open_path.cubic_to( + self.x + cx0, + self.y - cy0, + self.x + cx1, + self.y - cy1, + self.x + x, + self.y - y, + ); + } + + fn close(&mut self) { + self.open_path.close(); + } +} diff --git a/src/style/brush.rs b/src/style/brush.rs index 75b162c0..ad671f06 100644 --- a/src/style/brush.rs +++ b/src/style/brush.rs @@ -4,13 +4,4 @@ /// Trait for types that represent the color of glyphs or decorations. pub trait Brush: Clone + PartialEq + Default + core::fmt::Debug {} -/// Empty brush. -impl Brush for () {} - -/// Brush for a 4-byte color value. -impl Brush for [u8; 4] {} - -/// Brush for a 3-byte color value. -impl Brush for [u8; 3] {} - -impl Brush for peniko::Brush {} +impl Brush for T {}