diff --git a/examples/counter/src/main.rs b/examples/counter/src/main.rs index 5aad0987..762a0a92 100644 --- a/examples/counter/src/main.rs +++ b/examples/counter/src/main.rs @@ -1,4 +1,6 @@ use floem::{ + event::{Event, EventListener}, + keyboard::{Key, NamedKey}, peniko::Color, reactive::create_signal, unit::UnitExt, @@ -8,7 +10,7 @@ use floem::{ fn app_view() -> impl View { let (counter, set_counter) = create_signal(0); - stack(( + let view = stack(( label(move || format!("Value: {}", counter.get())).style(|s| s.padding(10.0)), stack(( text("Increment") @@ -72,6 +74,16 @@ fn app_view() -> impl View { .flex_col() .items_center() .justify_center() + }); + + let id = view.id(); + view.on_event(EventListener::KeyUp, move |e| { + if let Event::KeyUp(e) = e { + if e.key.logical_key == Key::Named(NamedKey::F11) { + id.inspect(); + } + } + true }) } diff --git a/examples/widget-gallery/src/main.rs b/examples/widget-gallery/src/main.rs index 62e6e7de..c368ca49 100644 --- a/examples/widget-gallery/src/main.rs +++ b/examples/widget-gallery/src/main.rs @@ -31,7 +31,7 @@ fn app_view() -> impl View { let (tabs, _set_tabs) = create_signal(tabs); let (active_tab, set_active_tab) = create_signal(0); - stack({ + let view = stack({ ( container({ scroll({ @@ -144,7 +144,17 @@ fn app_view() -> impl View { ) }) .style(|s| s.size(100.pct(), 100.pct())) - .window_title(|| "Widget Gallery".to_owned()) + .window_title(|| "Widget Gallery".to_owned()); + + let id = view.id(); + view.on_event(EventListener::KeyUp, move |e| { + if let Event::KeyUp(e) = e { + if e.key.logical_key == Key::Named(NamedKey::F11) { + id.inspect(); + } + } + true + }) } fn main() { diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 0c7fcf09..0fe36b44 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -20,7 +20,7 @@ pub struct Img<'a> { } pub trait Renderer { - fn begin(&mut self); + fn begin(&mut self, capture: bool); fn transform(&mut self, transform: Affine); @@ -49,5 +49,5 @@ pub trait Renderer { fn draw_img(&mut self, img: Img<'_>, rect: Rect); - fn finish(&mut self); + fn finish(&mut self) -> Option; } diff --git a/src/app.rs b/src/app.rs index f9dd7aef..f5d87a6d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,6 @@ use std::{cell::RefCell, sync::Arc}; +use floem_reactive::WriteSignal; use once_cell::sync::Lazy; use parking_lot::Mutex; use winit::{ @@ -8,7 +9,10 @@ use winit::{ window::WindowId, }; -use crate::{action::Timer, app_handle::ApplicationHandle, view::View, window::WindowConfig}; +use crate::{ + action::Timer, app_handle::ApplicationHandle, inspector::Capture, view::View, + window::WindowConfig, +}; type AppEventCallback = dyn Fn(AppEvent); @@ -42,6 +46,10 @@ pub(crate) enum AppUpdateEvent { CloseWindow { window_id: WindowId, }, + CaptureWindow { + window_id: WindowId, + capture: WriteSignal>, + }, RequestTimer { timer: Timer, }, diff --git a/src/app_handle.rs b/src/app_handle.rs index 8eb6e0c4..a7fd0be3 100644 --- a/src/app_handle.rs +++ b/src/app_handle.rs @@ -12,6 +12,7 @@ use crate::{ action::{Timer, TimerToken}, app::{AppUpdateEvent, UserEvent, APP_UPDATE_EVENTS}, ext_event::EXT_EVENT_HANDLER, + inspector::Capture, view::View, window::WindowConfig, window_handle::WindowHandle, @@ -64,6 +65,9 @@ impl ApplicationHandle { AppUpdateEvent::RequestTimer { timer } => { self.request_timer(timer, event_loop); } + AppUpdateEvent::CaptureWindow { window_id, capture } => { + capture.set(self.capture_window(window_id)); + } #[cfg(target_os = "linux")] AppUpdateEvent::MenuAction { window_id, @@ -234,6 +238,12 @@ impl ApplicationHandle { } } + fn capture_window(&mut self, window_id: WindowId) -> Option { + self.window_handles + .get_mut(&window_id) + .map(|handle| handle.capture()) + } + pub(crate) fn idle(&mut self) { while let Some(trigger) = { EXT_EVENT_HANDLER.queue.lock().pop_front() } { trigger.notify(); diff --git a/src/context.rs b/src/context.rs index 5a60021c..c6c077f2 100644 --- a/src/context.rs +++ b/src/context.rs @@ -261,6 +261,9 @@ pub struct AppState { pub(crate) keyboard_navigation: bool, pub(crate) window_menu: HashMap>, pub(crate) context_menu: HashMap>, + + /// This is set if we're currently capturing the window for the inspector. + pub(crate) capture: Option<()>, } impl Default for AppState { @@ -298,6 +301,7 @@ impl AppState { grid_bps: GridBreakpoints::default(), window_menu: HashMap::new(), context_menu: HashMap::new(), + capture: None, } } diff --git a/src/event.rs b/src/event.rs index 77671336..092d878b 100644 --- a/src/event.rs +++ b/src/event.rs @@ -9,7 +9,7 @@ use crate::{ pointer::{PointerInputEvent, PointerMoveEvent, PointerWheelEvent}, }; -#[derive(Hash, PartialEq, Eq)] +#[derive(Debug, Hash, PartialEq, Eq)] pub enum EventListener { KeyDown, KeyUp, diff --git a/src/id.rs b/src/id.rs index de82d802..312fc726 100644 --- a/src/id.rs +++ b/src/id.rs @@ -214,6 +214,10 @@ impl Id { self.add_update_message(UpdateMessage::PopoutMenu { id: *self, menu }); } + pub fn inspect(&self) { + self.add_update_message(UpdateMessage::Inspect); + } + fn add_update_message(&self, msg: UpdateMessage) { CENTRAL_UPDATE_MESSAGES.with(|msgs| { msgs.borrow_mut().push((*self, msg)); diff --git a/src/inspector.rs b/src/inspector.rs new file mode 100644 index 00000000..0b7c4f17 --- /dev/null +++ b/src/inspector.rs @@ -0,0 +1,457 @@ +use crate::app::{add_app_update_event, AppUpdateEvent}; +use crate::context::AppState; +use crate::event::{Event, EventListener}; +use crate::id::Id; +use crate::new_window; +use crate::view::View; +use crate::views::{ + dyn_container, empty, img_dynamic, list, scroll, stack, text, Decorators, Label, +}; +use crate::window::WindowConfig; +use floem_reactive::{create_rw_signal, RwSignal, Scope}; +use image::DynamicImage; +use kurbo::{Point, Rect, Size}; +use peniko::Color; +use std::cell::Cell; +use std::fmt::Display; +use std::rc::Rc; +use std::time::Instant; +use taffy::style::AlignItems; +use winit::window::WindowId; + +#[derive(Clone, Debug)] +pub struct CapturedView { + id: Id, + name: String, + layout: Rect, + clipped: Rect, + children: Vec>, +} + +impl CapturedView { + pub fn capture(view: &dyn View, app_state: &mut AppState, clip: Rect) -> Self { + let layout = app_state.get_layout_rect(view.id()); + let clipped = layout.intersect(clip); + Self { + id: view.id(), + name: view.debug_name().to_string(), + layout, + clipped, + children: view + .children() + .into_iter() + .map(|view| Rc::new(CapturedView::capture(view, app_state, clipped))) + .collect(), + } + } + + fn find(&self, id: Id) -> Option<&CapturedView> { + if self.id == id { + return Some(self); + } + self.children + .iter() + .filter_map(|child| child.find(id)) + .next() + } + + fn find_by_pos(&self, pos: Point) -> Option<&CapturedView> { + self.children + .iter() + .filter_map(|child| child.find_by_pos(pos)) + .next() + .or_else(|| self.clipped.contains(pos).then_some(self)) + } +} + +#[derive(Clone, Debug)] +pub struct Capture { + pub root: CapturedView, + pub start: Instant, + pub post_layout: Instant, + pub end: Instant, + pub window: Option>, +} + +impl Capture {} + +pub fn captured_view( + view: &CapturedView, + depth: usize, + selected: RwSignal>, + highlighted: RwSignal>, +) -> Box { + let offset = depth as f64 * 14.0; + let name = text(view.name.clone()); + let height = 20.0; + let id = view.id; + + if view.children.is_empty() { + return Box::new( + name.style(move |s| { + s.width_full() + .padding_left(20.0 + offset) + .hover(move |s| { + s.background(Color::rgba8(228, 237, 216, 160)) + .apply_if(selected.get() == Some(id), |s| { + s.background(Color::rgb8(186, 180, 216)) + }) + }) + .height(height) + .apply_if(highlighted.get() == Some(id), |s| { + s.background(Color::rgba8(228, 237, 216, 160)) + }) + .apply_if(selected.get() == Some(id), |s| { + if highlighted.get() == Some(id) { + s.background(Color::rgb8(186, 180, 216)) + } else { + s.background(Color::rgb8(213, 208, 216)) + } + }) + }) + .on_click(move |_| { + selected.set(Some(id)); + true + }) + .on_event(EventListener::PointerEnter, move |_| { + highlighted.set(Some(id)); + false + }), + ); + } + + let expanded = create_rw_signal(true); + + let row = stack(( + empty() + .style(move |s| { + s.background(if expanded.get() { + Color::WHITE.with_alpha_factor(0.3) + } else { + Color::BLACK.with_alpha_factor(0.3) + }) + .border(1.0) + .width(12.0) + .height(12.0) + .margin_left(offset) + .margin_right(4.0) + .border_color(Color::BLACK.with_alpha_factor(0.4)) + .border_radius(4.0) + .hover(move |s| { + s.border_color(Color::BLACK.with_alpha_factor(0.6)) + .background(if expanded.get() { + Color::WHITE.with_alpha_factor(0.5) + } else { + Color::BLACK.with_alpha_factor(0.5) + }) + }) + }) + .on_click(move |_| { + expanded.set(!expanded.get()); + true + }), + name, + )) + .style(move |s| { + s.width_full() + .padding_left(3.0) + .align_items(AlignItems::Center) + .hover(move |s| { + s.background(Color::rgba8(228, 237, 216, 160)) + .apply_if(selected.get() == Some(id), |s| { + s.background(Color::rgb8(186, 180, 216)) + }) + }) + .height(height) + .apply_if(highlighted.get() == Some(id), |s| { + s.background(Color::rgba8(228, 237, 216, 160)) + }) + .apply_if(selected.get() == Some(id), |s| { + if highlighted.get() == Some(id) { + s.background(Color::rgb8(186, 180, 216)) + } else { + s.background(Color::rgb8(213, 208, 216)) + } + }) + }) + .on_click(move |_| { + selected.set(Some(id)); + true + }) + .on_event(EventListener::PointerEnter, move |_| { + highlighted.set(Some(id)); + false + }); + + let children = view.children.clone(); + + let line = empty().style(move |s| { + s.absolute() + .height_full() + .width(1.0) + .margin_left(9.0 + offset) + .background(Color::BLACK.with_alpha_factor(0.1)) + }); + + let list = dyn_container( + move || expanded.get(), + move |expanded| { + if expanded { + let children = children.clone(); + Box::new( + list( + move || children.clone().into_iter().enumerate(), + |(i, _)| *i, + move |(_, child)| captured_view(&child, depth + 1, selected, highlighted), + ) + .style(|s| s.flex_col().width_full()), + ) + } else { + Box::new(empty()) + } + }, + ); + + let list = stack((line, list)).style(|s| s.flex_col().width_full()); + + Box::new(stack((row, list)).style(|s| s.flex_col().width_full())) +} + +fn header(label: impl Display) -> Label { + text(label).style(|s| { + s.padding(5.0) + .background(Color::WHITE_SMOKE) + .width_full() + .border_bottom(1.0) + .border_color(Color::LIGHT_GRAY) + }) +} + +fn stats(capture: &Capture) -> impl View { + let layout_time = capture.post_layout.saturating_duration_since(capture.start); + let paint_time = capture.end.saturating_duration_since(capture.post_layout); + let layout_time = text(format!( + "Layout time: {:.4} ms", + layout_time.as_secs_f64() * 1000.0 + )) + .style(|s| s.padding(5.0)); + let paint_time = text(format!( + "Paint time: {:.4} ms", + paint_time.as_secs_f64() * 1000.0 + )) + .style(|s| s.padding(5.0)); + stack((layout_time, paint_time)).style(|s| s.flex_col()) +} + +fn selected_view(capture: &Rc, selected: RwSignal>) -> impl View { + let capture = capture.clone(); + dyn_container( + move || selected.get(), + move |current| { + let info = |i| text(i).style(|s| s.padding(5.0)); + if let Some(view) = current.and_then(|id| capture.root.find(id)) { + let name = info(format!("Type: {}", view.name)); + let count = info(format!("Child Count: {}", view.children.len())); + let x = info(format!("X: {}", view.layout.x0)); + let y = info(format!("Y: {}", view.layout.y0)); + let w = info(format!("Width: {}", view.layout.width())); + let h = info(format!("Height: {}", view.layout.height())); + let clear = text("Clear selection") + .style(|s| { + s.background(Color::WHITE_SMOKE) + .border(1.0) + .padding(5.0) + .margin(5.0) + .border_color(Color::BLACK.with_alpha_factor(0.4)) + .border_radius(4.0) + .hover(move |s| { + s.border_color(Color::BLACK.with_alpha_factor(0.2)) + .background(Color::GRAY.with_alpha_factor(0.6)) + }) + }) + .on_click(move |_| { + selected.set(None); + true + }); + Box::new(stack((name, count, x, y, w, h, clear)).style(|s| s.flex_col())) + } else { + Box::new(info("No selection".to_string())) + } + }, + ) +} + +fn capture_view(capture: &Rc) -> impl View { + let selected = create_rw_signal(None); + let highlighted = create_rw_signal(None); + + let window = capture.window.clone(); + let capture_ = capture.clone(); + let capture__ = capture.clone(); + let image = img_dynamic(move || window.clone()) + .style(|s| { + s.margin(5.0) + .border(1.0) + .border_color(Color::BLACK.with_alpha_factor(0.5)) + .margin_bottom(25.0) + .margin_right(25.0) + }) + .on_event(EventListener::PointerMove, move |e| { + if let Event::PointerMove(e) = e { + if let Some(view) = capture_.root.find_by_pos(e.pos) { + highlighted.set(Some(view.id)); + return false; + } + } + if highlighted.get().is_some() { + highlighted.set(None); + } + false + }) + .on_click(move |e| { + if let Event::PointerUp(e) = e { + if let Some(view) = capture__.root.find_by_pos(e.pos) { + selected.set(Some(view.id)); + return true; + } + } + if selected.get().is_some() { + selected.set(None); + } + true + }) + .on_event(EventListener::PointerLeave, move |_| { + highlighted.set(None); + false + }); + + let capture_ = capture.clone(); + let selected_overlay = empty().style(move |s| { + if let Some(view) = selected.get().and_then(|id| capture_.root.find(id)) { + s.absolute() + .margin_left(5.0 + view.layout.x0) + .margin_top(5.0 + view.layout.y0) + .width(view.layout.width()) + .height(view.layout.height()) + .background(Color::rgb8(186, 180, 216).with_alpha_factor(0.5)) + .border_color(Color::rgb8(186, 180, 216).with_alpha_factor(0.7)) + .border(1.0) + } else { + s + } + }); + + let capture_ = capture.clone(); + let highlighted_overlay = empty().style(move |s| { + if let Some(view) = highlighted.get().and_then(|id| capture_.root.find(id)) { + s.absolute() + .margin_left(5.0 + view.layout.x0) + .margin_top(5.0 + view.layout.y0) + .width(view.layout.width()) + .height(view.layout.height()) + .background(Color::rgba8(228, 237, 216, 120)) + .border_color(Color::rgba8(75, 87, 53, 120)) + .border(1.0) + } else { + s + } + }); + + let image = stack((image, selected_overlay, highlighted_overlay)); + + let left = stack(( + header("Captured Window"), + scroll(image).style(|s| s.max_height_pct(60.0)), + header("Selected View"), + scroll(selected_view(capture, selected)), + header("Stats"), + scroll(stats(capture)), + )) + .style(|s| s.flex_col().height_full().max_width_pct(60.0)); + + let tree = stack(( + header("View Tree"), + scroll(captured_view(&capture.root, 0, selected, highlighted)) + .style(|s| s.width_full().height_full()) + .on_event(EventListener::PointerLeave, move |_| { + highlighted.set(None); + false + }) + .on_click(move |_| { + selected.set(None); + true + }), + )) + .style(|s| s.flex_col().width_full().height_full()); + + let seperator = empty().style(move |s| { + s.height_full() + .width(1.0) + .background(Color::BLACK.with_alpha_factor(0.2)) + }); + + stack((left, seperator, tree)).style(|s| s.flex_row().width_full().height_full()) +} + +fn inspector_view(capture: &Option>) -> impl View { + let view: Box = if let Some(capture) = capture { + Box::new(capture_view(capture)) + } else { + Box::new(text("No capture")) + }; + + stack((view,)) + .window_title(|| "Floem Inspector".to_owned()) + .on_event(EventListener::WindowClosed, |_| { + RUNNING.set(false); + false + }) + .style(|s| { + s.font_size(12.0) + .width_full() + .height_full() + .background(Color::WHITE) + .set(scroll::Thickness, 20.0) + .set(scroll::Rounded, false) + .set(scroll::HandleColor, Color::rgba8(166, 166, 166, 140)) + .set(scroll::DragColor, Color::rgb8(166, 166, 166)) + .set(scroll::HoverColor, Color::rgb8(184, 184, 184)) + .set(scroll::BgActiveColor, Color::rgba8(166, 166, 166, 40)) + }) +} + +thread_local! { + pub(crate) static RUNNING: Cell = Cell::new(false); + pub(crate) static CAPTURE: RwSignal> = { + Scope::new().create_rw_signal(None) + }; +} + +pub fn capture(window_id: WindowId) { + let capture = CAPTURE.with(|c| *c); + + if !RUNNING.get() { + RUNNING.set(true); + new_window( + move |_| { + dyn_container( + move || capture.get(), + |capture| Box::new(inspector_view(&capture.map(Rc::new))), + ) + .style(|s| s.width_full().height_full()) + }, + Some(WindowConfig { + size: Some(Size { + width: 1200.0, + height: 800.0, + }), + ..Default::default() + }), + ); + } + + add_app_update_event(AppUpdateEvent::CaptureWindow { + window_id, + capture: capture.write_only(), + }) +} diff --git a/src/lib.rs b/src/lib.rs index a7af5213..dd5ae7fe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,6 +98,7 @@ pub mod event; pub mod ext_event; pub mod file; pub mod id; +mod inspector; pub mod keyboard; pub mod menu; pub mod pointer; diff --git a/src/renderer.rs b/src/renderer.rs index 291b0a4f..8aa0efb8 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -51,6 +51,7 @@ use crate::cosmic_text::TextLayout; use floem_renderer::Img; use floem_tiny_skia::TinySkiaRenderer; use floem_vger::VgerRenderer; +use image::DynamicImage; use kurbo::{Affine, Rect, Shape, Size}; use peniko::BrushRef; @@ -99,13 +100,13 @@ impl Renderer { } impl floem_renderer::Renderer for Renderer { - fn begin(&mut self) { + fn begin(&mut self, capture: bool) { match self { Renderer::Vger(r) => { - r.begin(); + r.begin(capture); } Renderer::TinySkia(r) => { - r.begin(); + r.begin(capture); } } } @@ -219,14 +220,10 @@ impl floem_renderer::Renderer for Renderer { } } - fn finish(&mut self) { + fn finish(&mut self) -> Option { match self { - Renderer::Vger(r) => { - r.finish(); - } - Renderer::TinySkia(r) => { - r.finish(); - } + Renderer::Vger(r) => r.finish(), + Renderer::TinySkia(r) => r.finish(), } } } diff --git a/src/update.rs b/src/update.rs index 53521a22..3ebac7ff 100644 --- a/src/update.rs +++ b/src/update.rs @@ -114,6 +114,7 @@ pub(crate) enum UpdateMessage { SetWindowTitle { title: String, }, + Inspect, FocusWindow, SetImeAllowed { allowed: bool, diff --git a/src/views/img.rs b/src/views/img.rs index af6fda6d..5e0b58e2 100644 --- a/src/views/img.rs +++ b/src/views/img.rs @@ -1,3 +1,5 @@ +use std::rc::Rc; + use floem_reactive::create_effect; use floem_renderer::Renderer; use image::{DynamicImage, GenericImageView}; @@ -87,22 +89,23 @@ impl ImageStyle { pub struct Img { id: Id, //FIXME: store the pixel format(once its added to vger), for now we only store RGBA(RGB is converted to RGBA) - pixels: Option>, - img: Option, + img: Option>, img_hash: Option>, img_dimensions: Option<(u32, u32)>, content_node: Option, } pub fn img(image: impl Fn() -> Vec + 'static) -> Img { + img_dynamic(move || image::load_from_memory(&image()).ok().map(Rc::new)) +} + +pub(crate) fn img_dynamic(image: impl Fn() -> Option> + 'static) -> Img { let id = Id::next(); create_effect(move |_| { - let img_data = image(); - id.update_state(img_data, false); + id.update_state(image(), false); }); Img { id, - pixels: None, img: None, img_hash: None, img_dimensions: None, @@ -140,23 +143,17 @@ impl View for Img { cx: &mut crate::context::UpdateCx, state: Box, ) -> crate::view::ChangeFlags { - if let Ok(state) = state.downcast::>() { - let image = &*state; - - let img = image::load_from_memory(image).ok(); - self.img = img; - self.pixels = Some(image.clone()); + if let Ok(img) = state.downcast::>>() { + self.img_hash = (*img).as_ref().map(|img| { + let mut hasher = Sha256::new(); + hasher.update(img.as_bytes()); + hasher.finalize().to_vec() + }); + self.img = *img; self.img_dimensions = self.img.as_ref().map(|img| img.dimensions()); - - let mut hasher = Sha256::new(); - hasher.update(image); - let hash = hasher.finalize().to_vec(); - - self.img_hash = Some(hash); cx.request_layout(self.id()); ChangeFlags::LAYOUT } else { - eprintln!("downcast failed"); ChangeFlags::empty() } } @@ -202,7 +199,7 @@ impl View for Img { cx.draw_img( floem_renderer::Img { img, - data: self.pixels.as_ref().unwrap(), + data: img.as_bytes(), hash: self.img_hash.as_ref().unwrap(), }, rect, diff --git a/src/window_handle.rs b/src/window_handle.rs index 8f7d5613..c16d1dae 100644 --- a/src/window_handle.rs +++ b/src/window_handle.rs @@ -1,16 +1,17 @@ -use std::time::{Duration, Instant}; +use std::{ + rc::Rc, + time::{Duration, Instant}, +}; use floem_reactive::{with_scope, RwSignal, Scope}; use floem_renderer::Renderer; +use image::DynamicImage; use kurbo::{Affine, Point, Rect, Size, Vec2}; - -#[cfg(target_os = "linux")] -use winit::window::WindowId; use winit::{ dpi::{LogicalPosition, LogicalSize}, event::{ElementState, Ime, MouseButton, MouseScrollDelta}, keyboard::{Key, ModifiersState, NamedKey}, - window::{CursorIcon, Theme}, + window::{CursorIcon, Theme, WindowId}, }; #[cfg(target_os = "linux")] @@ -25,6 +26,7 @@ use crate::{ }, event::{Event, EventListener}, id::{Id, IdPath, ID_PATHS}, + inspector::{self, Capture, CapturedView}, keyboard::KeyEvent, menu::Menu, pointer::{PointerButton, PointerInputEvent, PointerMoveEvent, PointerWheelEvent}, @@ -45,6 +47,7 @@ use crate::{ /// - requesting a new animation frame from the backend pub(crate) struct WindowHandle { pub(crate) window: Option, + window_id: WindowId, id: Id, /// Reactive Scope for this WindowHandle scope: Scope, @@ -107,6 +110,7 @@ impl WindowHandle { let paint_state = PaintState::new(&window, scale, size.get_untracked() * scale); let mut window_handle = Self { window: Some(window), + window_id, id, scope, view, @@ -457,13 +461,15 @@ impl WindowHandle { let id = self.app_state.ids_with_anim_in_progress().get(0).cloned(); if let Some(id) = id { - exec_after(Duration::from_millis(1), move |_| { - id.request_layout(); - }); + if self.app_state.capture.is_none() { + exec_after(Duration::from_millis(1), move |_| { + id.request_layout(); + }); + } } } - pub fn paint(&mut self) { + pub fn paint(&mut self) -> Option { let mut cx = PaintCx { app_state: &mut self.app_state, paint_state: &mut self.paint_state, @@ -492,13 +498,53 @@ impl WindowHandle { saved_scroll_bar_thicknesses: Vec::new(), saved_scroll_bar_edge_widths: Vec::new(), }; - cx.paint_state.renderer.begin(); + cx.paint_state + .renderer + .begin(cx.app_state.capture.is_some()); self.view.paint_main(&mut cx); if let Some(window) = self.window.as_ref() { - window.pre_present_notify(); + if cx.app_state.capture.is_none() { + window.pre_present_notify(); + } + } + let image = cx.paint_state.renderer.finish(); + + if cx.app_state.capture.is_none() { + self.process_update(); } - cx.paint_state.renderer.finish(); + + image + } + + pub(crate) fn capture(&mut self) -> Capture { + // Ensure we run layout again for accurate timing. + self.app_state + .view_states + .values_mut() + .for_each(|state| state.request_layout = true); + self.app_state.capture = Some(()); + + let start = Instant::now(); + + self.layout(); + let post_layout = Instant::now(); + let window = self.paint().map(Rc::new); + let end = Instant::now(); + + self.app_state.capture = None; + + let root_layout = self.app_state.get_layout_rect(self.view.id()); + let capture = Capture { + start, + post_layout, + end, + window, + root: CapturedView::capture(&self.view, &mut self.app_state, root_layout), + }; + // Process any updates produced by capturing self.process_update(); + + capture } pub(crate) fn process_update(&mut self) { @@ -779,6 +825,9 @@ impl WindowHandle { ); } } + UpdateMessage::Inspect => { + inspector::capture(self.window_id); + } } } } diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index 32d69c1e..cd1fe82d 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -6,6 +6,7 @@ use floem_renderer::tiny_skia::{ }; use floem_renderer::Img; use floem_renderer::Renderer; +use image::DynamicImage; use peniko::kurbo::PathEl; use peniko::{ kurbo::{Affine, Point, Rect, Shape}, @@ -338,7 +339,7 @@ impl TinySkiaRenderer { } impl Renderer for TinySkiaRenderer { - fn begin(&mut self) { + fn begin(&mut self, _capture: bool) { self.transform = Affine::IDENTITY; self.pixmap.fill(tiny_skia::Color::WHITE); self.clip = None; @@ -521,7 +522,7 @@ impl Renderer for TinySkiaRenderer { self.clip = None; } - fn finish(&mut self) { + fn finish(&mut self) -> Option { // Remove cache entries which were not accessed. self.image_cache.retain(|_, (c, _)| *c == self.cache_color); self.glyph_cache.retain(|_, (c, _)| *c == self.cache_color); @@ -543,5 +544,7 @@ impl Renderer for TinySkiaRenderer { buffer .present() .expect("failed to present the surface buffer"); + + None } } diff --git a/vger/src/lib.rs b/vger/src/lib.rs index d819686d..e6d57009 100644 --- a/vger/src/lib.rs +++ b/vger/src/lib.rs @@ -1,9 +1,10 @@ +use std::sync::mpsc::sync_channel; use std::sync::Arc; use anyhow::Result; use floem_renderer::cosmic_text::{SubpixelBin, SwashCache, TextLayout}; use floem_renderer::{tiny_skia, Img, Renderer}; -use image::EncodableLayout; +use image::{DynamicImage, EncodableLayout, RgbaImage}; use peniko::{ kurbo::{Affine, Point, Rect, Shape, Vec2}, BrushRef, Color, GradientKind, @@ -21,8 +22,16 @@ pub struct VgerRenderer { scale: f64, transform: Affine, clip: Option, + capture: bool, } +const CLEAR_COLOR: wgpu::Color = wgpu::Color { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, +}; + impl VgerRenderer { pub fn new< W: raw_window_handle::HasRawDisplayHandle + raw_window_handle::HasRawWindowHandle, @@ -101,6 +110,7 @@ impl VgerRenderer { config, transform: Affine::IDENTITY, clip: None, + capture: false, }) } @@ -161,10 +171,101 @@ impl VgerRenderer { let size = (end - origin).to_size(); vger::defs::LocalRect::new(origin, size) } + + fn render_image(&mut self) -> Option { + let width_align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT - 1; + let width = (self.config.width + width_align) & !width_align; + let height = self.config.height; + let texture_desc = wgpu::TextureDescriptor { + size: wgpu::Extent3d { + width: self.config.width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, + label: Some("render_texture"), + view_formats: &[wgpu::TextureFormat::Rgba8Unorm], + }; + let texture = self.device.create_texture(&texture_desc); + let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + let desc = wgpu::RenderPassDescriptor { + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(CLEAR_COLOR), + store: StoreOp::Store, + }, + })], + ..Default::default() + }; + + self.vger.encode(&desc); + + let bytes_per_pixel = 4; + let buffer = self.device.create_buffer(&wgpu::BufferDescriptor { + label: None, + size: (width as u64 * height as u64 * bytes_per_pixel), + usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let bytes_per_row = width * bytes_per_pixel as u32; + assert!(bytes_per_row % wgpu::COPY_BYTES_PER_ROW_ALIGNMENT == 0); + + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + encoder.copy_texture_to_buffer( + texture.as_image_copy(), + wgpu::ImageCopyBuffer { + buffer: &buffer, + layout: wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(bytes_per_row), + rows_per_image: None, + }, + }, + texture_desc.size, + ); + let command_buffer = encoder.finish(); + self.queue.submit(Some(command_buffer)); + self.device.poll(wgpu::Maintain::Wait); + + let slice = buffer.slice(..); + let (tx, rx) = sync_channel(1); + slice.map_async(wgpu::MapMode::Read, move |r| tx.send(r).unwrap()); + + loop { + if let Ok(r) = rx.try_recv() { + break r.ok()?; + } + if self.device.poll(wgpu::MaintainBase::Wait) { + rx.recv().ok()?.ok()?; + break; + } + } + + let mut cropped_buffer = Vec::new(); + let buffer: Vec = slice.get_mapped_range().to_owned(); + + let mut cursor = 0; + let row_size = self.config.width as usize * bytes_per_pixel as usize; + for _ in 0..height { + cropped_buffer.extend_from_slice(&buffer[cursor..(cursor + row_size)]); + cursor += bytes_per_row as usize; + } + + RgbaImage::from_raw(self.config.width, height, cropped_buffer).map(DynamicImage::ImageRgba8) + } } impl Renderer for VgerRenderer { - fn begin(&mut self) { + fn begin(&mut self, capture: bool) { + self.capture = capture; self.transform = Affine::IDENTITY; self.vger.begin( self.config.width as f32, @@ -431,33 +532,33 @@ impl Renderer for VgerRenderer { self.clip = None; } - fn finish(&mut self) { - if let Ok(frame) = self.surface.get_current_texture() { - let texture_view = frame - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - let desc = wgpu::RenderPassDescriptor { - label: None, - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &texture_view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { - r: 0.0, - g: 0.0, - b: 0.0, - a: 0.0, - }), - store: StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }; - - self.vger.encode(&desc); - frame.present(); + fn finish(&mut self) -> Option { + if self.capture { + self.render_image() + } else { + if let Ok(frame) = self.surface.get_current_texture() { + let texture_view = frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let desc = wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &texture_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(CLEAR_COLOR), + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }; + + self.vger.encode(&desc); + frame.present(); + } + None } } }