Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
309 changes: 246 additions & 63 deletions Cargo.lock

Large diffs are not rendered by default.

24 changes: 15 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,14 @@ taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "25759dad44d8350743
] }

# AnyRender
anyrender = { version = "0.7" }
anyrender_vello = { version = "0.7" }
anyrender_vello_cpu = { version = "0.9" }
anyrender_vello_hybrid = { version = "0.2" }
anyrender_skia = { version = "0.4" }
anyrender_svg = { version = "0.8" }
wgpu_context = { version = "0.3" }
anyrender = { git = "https://github.com/DioxusLabs/anyrender", rev = "f7d5b67c1f65f4e2f037216d3515e12bca8139e5", version = "0.7" }
anyrender_serialize = { git = "https://github.com/DioxusLabs/anyrender", rev = "f7d5b67c1f65f4e2f037216d3515e12bca8139e5", version = "0.1" }
anyrender_vello = { git = "https://github.com/DioxusLabs/anyrender", rev = "f7d5b67c1f65f4e2f037216d3515e12bca8139e5", version = "0.7" }
anyrender_vello_cpu = { git = "https://github.com/DioxusLabs/anyrender", rev = "f7d5b67c1f65f4e2f037216d3515e12bca8139e5", version = "0.9" }
anyrender_vello_hybrid = { git = "https://github.com/DioxusLabs/anyrender", rev = "f7d5b67c1f65f4e2f037216d3515e12bca8139e5", version = "0.2" }
anyrender_skia = { git = "https://github.com/DioxusLabs/anyrender", rev = "f7d5b67c1f65f4e2f037216d3515e12bca8139e5", version = "0.4" }
anyrender_svg = { git = "https://github.com/DioxusLabs/anyrender", rev = "f7d5b67c1f65f4e2f037216d3515e12bca8139e5", version = "0.8" }
wgpu_context = { git = "https://github.com/DioxusLabs/anyrender", rev = "f7d5b67c1f65f4e2f037216d3515e12bca8139e5", version = "0.3" }

# Linebender + Fontations + WGPU + SVG
color = "0.3"
Expand All @@ -130,7 +131,7 @@ usvg = "0.46"

# Windowing & Input
raw-window-handle = "0.6.0"
winit = { version = "=0.31.0-beta.2" }
winit = { path = "../winit/winit" }
accesskit = "0.24"
arboard = { version = "3.4.1", default-features = false }
rfd = { version = "0.17.1", default-features = false }
Expand Down Expand Up @@ -231,7 +232,12 @@ blitz-shell = { workspace = true }
blitz-net = { workspace = true }
blitz = { workspace = true, features = ["net"] }
dioxus = { workspace = true }
dioxus-native = { workspace = true, features = ["vello", "floats", "svg", "prelude"] }
dioxus-native = { workspace = true, features = [
"vello",
"floats",
"svg",
"prelude",
] }
euclid = { workspace = true }
reqwest = { workspace = true }
tokio = { workspace = true, features = ["macros"] }
Expand Down
9 changes: 6 additions & 3 deletions apps/browser/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ name = "blitz"
path = "src/main.rs"

[features]
default = ["vello", "floats", "incremental", "cookies", "cache", "screenshot"]
default = ["vello", "floats", "incremental", "cookies", "cache", "screenshot", "capture"]
screenshot = ["dep:anyrender", "dep:anyrender_vello_cpu", "dep:blitz-paint", "dep:png", "dep:peniko", "dep:rfd"]
capture = ["dep:anyrender", "dep:anyrender_serialize", "dep:blitz-paint", "dep:png", "dep:peniko", "dep:rfd"]
vello = ["dioxus-native/vello"]
hybrid = ["dioxus-native/vello-hybrid"]
skia = ["dioxus-native/skia"]
Expand Down Expand Up @@ -51,15 +52,17 @@ webbrowser = { workspace = true }
winit = { workspace = true }
image = { workspace = true }
anyrender = { workspace = true, optional = true }
anyrender_serialize = { workspace = true, optional = true }
anyrender_vello_cpu = { workspace = true, optional = true }
blitz-paint = { workspace = true, optional = true }
png = { workspace = true, optional = true }
peniko = { workspace = true, optional = true }
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
rfd = { workspace = true, features = ["xdg-portal"], optional = true }

# Allocators
mimalloc = { version = "0.1.48", optional = true }

[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
rfd = { workspace = true, features = ["xdg-portal"], optional = true }

[target.'cfg(target_os = "android")'.dependencies]
android-activity = { version = "0.6.0", features = ["native-activity"] }
96 changes: 73 additions & 23 deletions apps/browser/src/capture.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,52 @@
//! Utility functions for capturing screenshots

#[cfg(feature = "screenshot")]
use anyrender::{PaintScene as _, render_to_buffer};
#[cfg(feature = "screenshot")]
use anyrender_vello_cpu::VelloCpuImageRenderer;
#[cfg(feature = "screenshot")]
use anyrender::PaintScene;
use blitz_paint::paint_scene;
#[cfg(feature = "screenshot")]
use peniko::Fill;
#[cfg(feature = "screenshot")]
use peniko::kurbo::Rect;
use std::path::{Path, PathBuf};

#[cfg(feature = "screenshot")]
use std::path::Path;
use std::path::PathBuf;
use anyrender::render_to_buffer;
#[cfg(feature = "screenshot")]
use anyrender_vello_cpu::VelloCpuImageRenderer;

#[cfg(feature = "capture")]
use anyrender_serialize::{SceneArchive, SerializeConfig};

#[derive(Copy, Clone)]
pub(crate) enum RenderSize {
/// Render the scene at the size of the
Viewport,
#[allow(unused)]
/// Render the scene using the size of the full document height
FullDocumentHeight,
}

impl RenderSize {
fn resolve(&self, doc: &blitz_dom::BaseDocument) -> (u32, u32) {
match self {
RenderSize::Viewport => doc.viewport().window_size,
RenderSize::FullDocumentHeight => {
let root_element_size = doc.root_element().final_layout.size;
(
root_element_size.width as u32,
root_element_size.height as u32,
)
}
}
}
}

/// Capture a screenshot as PNG and write it to the specified path
#[cfg(feature = "screenshot")]
pub(crate) fn capture_screenshot(doc: &blitz_dom::BaseDocument, path: &Path) {
let viewport = doc.viewport();
let scale = viewport.scale_f64();
let (render_width, render_height) = viewport.window_size;
let size = RenderSize::Viewport;
let (render_width, render_height) = size.resolve(doc);

let buffer = render_to_buffer::<VelloCpuImageRenderer, _>(
|scene| {
scene.fill(
Fill::NonZero,
Default::default(),
blitz_dom::util::Color::WHITE,
Default::default(),
&Rect::new(0.0, 0.0, render_width as f64, render_height as f64),
);
paint_scene(scene, doc, scale, render_width, render_height, 0, 0);
render_scene(doc, scene, size);
},
render_width,
render_height,
Expand All @@ -49,21 +64,56 @@ pub(crate) fn capture_screenshot(doc: &blitz_dom::BaseDocument, path: &Path) {
}
}

/// Capture a scene as an AnyRender serialized scene
#[cfg(feature = "capture")]
pub(crate) fn capture_anyrender_scene(doc: &blitz_dom::BaseDocument, path: &Path) {
let mut scene = anyrender::Scene::new();
render_scene(doc, &mut scene, RenderSize::Viewport);

let config = SerializeConfig::new()
.with_woff2_fonts(true)
.with_subset_fonts(true);
let archive = SceneArchive::from_scene(&scene, &config).unwrap();

let mut file = std::fs::File::create(path).unwrap();
archive.serialize(&mut file).unwrap();
}

fn render_scene(
doc: &blitz_dom::BaseDocument,
scene: &mut impl PaintScene,
size: RenderSize,
) -> (u32, u32) {
let scale = doc.viewport().scale_f64();
let (render_width, render_height) = size.resolve(doc);

scene.fill(
Fill::NonZero,
Default::default(),
blitz_dom::util::Color::WHITE,
Default::default(),
&Rect::new(0.0, 0.0, render_width as f64, render_height as f64),
);
paint_scene(scene, doc, scale, render_width, render_height, 0, 0);

(render_width, render_height)
}

/// Open an RFD file dialog to get a path to save a file to
pub(crate) async fn try_get_save_path() -> Option<PathBuf> {
pub(crate) async fn try_get_save_path(file_type_name: &str, ext: &str) -> Option<PathBuf> {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let default_name = format!("blitz-screenshot-{timestamp}.png");
let default_name = format!("blitz-screenshot-{timestamp}.{ext}");

#[cfg(any(target_os = "android", target_os = "ios"))]
let path = Some(std::path::PathBuf::from(&default_name));

#[cfg(not(any(target_os = "android", target_os = "ios")))]
let path = rfd::AsyncFileDialog::new()
.set_file_name(&default_name)
.add_filter("PNG Image", &["png"])
.add_filter(file_type_name, &[ext])
.save_file()
.await
.map(|file| file.path().to_owned());
Expand Down
2 changes: 2 additions & 0 deletions apps/browser/src/icons.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ pub const FORWARDS_ICON: Asset = asset!("../assets/icons/arrow-right.svg");
pub const MENU_ICON: Asset = asset!("../assets/icons/ellipsis-vertical.svg");
pub const EXTERNAL_LINK_ICON: Asset = asset!("../assets/icons/external-link.svg");
pub const CODE_ICON: Asset = asset!("../assets/icons/code.svg");

#[cfg(any(feature = "screenshot", feature = "capture"))]
pub const CAMERA_ICON: Asset = asset!("../assets/icons/camera.svg");

#[component]
Expand Down
70 changes: 48 additions & 22 deletions apps/browser/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ use linebender_resource_handle::Blob;

type StdNetProvider = blitz_net::Provider;

#[cfg(any(feature = "screenshot", feature = "capture"))]
mod capture;

mod icons;
use icons::IconButton;

Expand Down Expand Up @@ -134,7 +136,7 @@ fn app() -> Element {
let screenshot_action = use_callback(move |_| {
menu_open.set(false);
async move {
let Some(path) = capture::try_get_save_path().await else {
let Some(path) = capture::try_get_save_path("PNG Image", "png").await else {
return;
};

Expand All @@ -153,6 +155,29 @@ fn app() -> Element {
}
});

#[cfg(feature = "capture")]
let capture_action = use_callback(move |_| {
menu_open.set(false);
async move {
let Some(path) = capture::try_get_save_path("AnyRender Scene", "scene").await else {
return;
};

if let Some(handle) = webview_node_handle() {
let node_id = handle.node_id();
let mut doc = handle.doc_mut();
if let Some(sub_doc) = doc
.get_node_mut(node_id)
.and_then(|node| node.element_data_mut())
.and_then(|el| el.sub_doc_data_mut())
{
let sub_doc = sub_doc.inner();
capture::capture_anyrender_scene(&sub_doc, &path);
}
}
}
});

let devtools_action = use_callback(move |_| {
menu_open.set(false);
if let Some(handle) = webview_node_handle() {
Expand All @@ -169,23 +194,28 @@ fn app() -> Element {
}
});

// HACK: Winit doesn't support "safe area" on Android yet.
// So we just hardcode a fallback safe area.
const TOP_PAD: &str = if cfg!(target_os = "android") {
"30px"
} else {
""
};
const BOTTOM_PAD: &str = if cfg!(target_os = "android") {
"44px"
} else {
""
};
#[cfg(feature = "screenshot")]
let screenshot_item = rsx!(
div { class: "menu-item", onclick: move |_| screenshot_action(()),
img { class: "menu-item-icon", src: icons::CAMERA_ICON }
"Capture Screenshot"
}
);
#[cfg(not(feature = "screenshot"))]
let screenshot_item = rsx!();

#[cfg(feature = "capture")]
let capture_item = rsx!(
div { class: "menu-item", onclick: move |_| capture_action(()),
img { class: "menu-item-icon", src: icons::CAMERA_ICON }
"Capture AnyRender Archive"
}
);
#[cfg(not(feature = "capture"))]
let capture_item = rsx!();

rsx!(
div { id: "frame",
padding_top: TOP_PAD,
padding_bottom: BOTTOM_PAD,
class: if IS_MOBILE {
"mobile"
} else {
Expand Down Expand Up @@ -264,7 +294,7 @@ fn app() -> Element {
},
oninput: move |evt| { *url_input_value.write() = evt.value() },
}

div { class: "menu-wrapper",
IconButton { icon: icons::MENU_ICON, action: move |_| menu_open.toggle(), active: menu_open() },
if menu_open() {
Expand All @@ -277,12 +307,8 @@ fn app() -> Element {
img { class: "menu-item-icon", src: icons::CODE_ICON }
"View Source"
}
if cfg!(feature = "screenshot") {
div { class: "menu-item", onclick: move |_| screenshot_action(()),
img { class: "menu-item-icon", src: icons::CAMERA_ICON }
"Capture Screenshot"
}
}
{screenshot_item}
{capture_item}
div { class: "menu-item", onclick: move |_| devtools_action(()), "Toggle DevTools" }
}
}
Expand Down
2 changes: 2 additions & 0 deletions packages/blitz-shell/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ blitz-traits = { workspace = true }
blitz-dom = { workspace = true }
blitz-paint = { workspace = true }
anyrender = { workspace = true }
kurbo = { workspace = true }
peniko = { workspace = true }

# Windowing & Input
winit = { workspace = true }
Expand Down
12 changes: 12 additions & 0 deletions packages/blitz-shell/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ use winit::event_loop::ActiveEventLoop;
use winit::window::{Theme, WindowAttributes, WindowId};
use winit::{event::Modifiers, event::WindowEvent, keyboard::KeyCode, window::Window};

#[cfg(target_os = "android")]
use anyrender::PaintScene as _;

#[cfg(feature = "accessibility")]
use crate::accessibility::AccessibilityState;

Expand Down Expand Up @@ -279,6 +282,15 @@ impl<Rend: WindowRenderer> View<Rend> {
let is_animating = inner.is_animating();
let insets = self.safe_area_insets.to_logical(scale);
self.renderer.render(|scene| {
#[cfg(target_os = "android")]
scene.fill(
peniko::Fill::NonZero,
kurbo::Affine::IDENTITY,
peniko::Color::BLACK,
None,
&kurbo::Rect::new(0.0, 0.0, width as f64, height as f64),
);

paint_scene(scene, &inner, scale, width, height, insets.left, insets.top)
});

Expand Down
Loading