From 55d1aaa6c4af237190929ec7c11dd693d37f5368 Mon Sep 17 00:00:00 2001 From: Joe Neeman Date: Thu, 16 Apr 2020 12:07:04 -0500 Subject: [PATCH 1/7] Implement partial invalidation. Fixes #402. Widgets can request partial invalidation using `request_paint_rect`. All of the partial invalidation requests will be combined into a single bounding rectangle, and passed to the system. When painting widgets, they have access to the dirty region. With this commit, widgets that paint outside their paint rects can cause graphical glitches. These widgets are buggy, but their bugs weren't so visible before (see #628). --- druid-shell/examples/invalidate.rs | 107 ++++++++++++++++ druid-shell/examples/perftest.rs | 2 +- druid-shell/examples/shello.rs | 2 +- druid-shell/src/platform/gtk/window.rs | 33 +++-- druid-shell/src/platform/mac/window.rs | 20 ++- druid-shell/src/platform/web/window.rs | 31 ++++- druid-shell/src/platform/windows/window.rs | 73 +++++++++-- druid-shell/src/platform/x11/application.rs | 10 +- druid-shell/src/platform/x11/window.rs | 11 +- druid-shell/src/window.rs | 12 +- druid/examples/invalidation.rs | 88 ++++++++++++++ druid/examples/wasm/src/lib.rs | 1 + druid/src/contexts.rs | 128 ++++++++++++++++---- druid/src/core.rs | 57 +++++++-- druid/src/tests/harness.rs | 10 +- druid/src/tests/helpers.rs | 6 +- druid/src/widget/invalidation.rs | 66 ++++++++++ druid/src/widget/mod.rs | 2 + druid/src/widget/scroll.rs | 3 +- druid/src/widget/widget_ext.rs | 10 +- druid/src/win_handler.rs | 14 +-- druid/src/window.rs | 14 ++- 22 files changed, 612 insertions(+), 88 deletions(-) create mode 100644 druid-shell/examples/invalidate.rs create mode 100644 druid/examples/invalidation.rs create mode 100644 druid/src/widget/invalidation.rs diff --git a/druid-shell/examples/invalidate.rs b/druid-shell/examples/invalidate.rs new file mode 100644 index 0000000000..5497127541 --- /dev/null +++ b/druid-shell/examples/invalidate.rs @@ -0,0 +1,107 @@ +// Copyright 2020 The xi-editor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::any::Any; + +use std::time::Instant; + +use piet_common::kurbo::{Point, Rect}; +use piet_common::{Color, Piet, RenderContext}; + +use druid_shell::{Application, WinHandler, WindowBuilder, WindowHandle}; + +struct InvalidateTest { + handle: WindowHandle, + size: (f64, f64), + start_time: Instant, + color: Color, + rect: Rect, +} + +fn split_rgb(c: &Color) -> (u8, u8, u8) { + let rgba = c.as_rgba_u32(); + ( + (rgba >> 24 & 255) as u8, + (rgba >> 16 & 255) as u8, + (rgba >> 8 & 255) as u8, + ) +} + +impl InvalidateTest { + fn update_color_and_rect(&mut self) { + let time_since_start = (Instant::now() - self.start_time).as_nanos(); + let (r, g, b) = split_rgb(&self.color); + self.color = match (time_since_start % 2, time_since_start % 3) { + (0, _) => Color::rgb8(r.wrapping_add(10), g, b), + (_, 0) => Color::rgb8(r, g.wrapping_add(10), b), + (_, _) => Color::rgb8(r, g, b.wrapping_add(10)), + }; + + self.rect.x0 = (self.rect.x0 + 5.0) % self.size.0; + self.rect.x1 = (self.rect.x1 + 5.5) % self.size.0; + self.rect.y0 = (self.rect.y0 + 3.0) % self.size.1; + self.rect.y1 = (self.rect.y1 + 3.5) % self.size.1; + } +} + +impl WinHandler for InvalidateTest { + fn connect(&mut self, handle: &WindowHandle) { + self.handle = handle.clone(); + } + + fn paint(&mut self, piet: &mut Piet, rect: Rect) -> bool { + self.update_color_and_rect(); + piet.fill(rect, &self.color); + + self.handle.invalidate_rect(self.rect); + false + } + + fn size(&mut self, width: u32, height: u32) { + let dpi = self.handle.get_dpi(); + let dpi_scale = dpi as f64 / 96.0; + let width_f = (width as f64) / dpi_scale; + let height_f = (height as f64) / dpi_scale; + self.size = (width_f, height_f); + } + + fn command(&mut self, id: u32) { + match id { + 0x100 => self.handle.close(), + _ => println!("unexpected id {}", id), + } + } + + fn as_any(&mut self) -> &mut dyn Any { + self + } +} + +fn main() { + let mut app = Application::new(None); + let mut builder = WindowBuilder::new(); + let inv_test = InvalidateTest { + size: Default::default(), + handle: Default::default(), + start_time: Instant::now(), + rect: Rect::from_origin_size(Point::ZERO, (10.0, 20.0)), + color: Color::WHITE, + }; + builder.set_handler(Box::new(inv_test)); + builder.set_title("Invalidate tester"); + + let window = builder.build().unwrap(); + window.show(); + app.run(); +} diff --git a/druid-shell/examples/perftest.rs b/druid-shell/examples/perftest.rs index d8a1a6cb39..16fbffba21 100644 --- a/druid-shell/examples/perftest.rs +++ b/druid-shell/examples/perftest.rs @@ -36,7 +36,7 @@ impl WinHandler for PerfTest { self.handle = handle.clone(); } - fn paint(&mut self, piet: &mut Piet) -> bool { + fn paint(&mut self, piet: &mut Piet, _: Rect) -> bool { let (width, height) = self.size; let rect = Rect::new(0.0, 0.0, width, height); piet.fill(rect, &BG_COLOR); diff --git a/druid-shell/examples/shello.rs b/druid-shell/examples/shello.rs index 702973e69f..14e41ab102 100644 --- a/druid-shell/examples/shello.rs +++ b/druid-shell/examples/shello.rs @@ -36,7 +36,7 @@ impl WinHandler for HelloState { self.handle = handle.clone(); } - fn paint(&mut self, piet: &mut piet_common::Piet) -> bool { + fn paint(&mut self, piet: &mut piet_common::Piet, _: Rect) -> bool { let (width, height) = self.size; let rect = Rect::new(0.0, 0.0, width, height); piet.fill(rect, &BG_COLOR); diff --git a/druid-shell/src/platform/gtk/window.rs b/druid-shell/src/platform/gtk/window.rs index bf726979cb..542395b80f 100644 --- a/druid-shell/src/platform/gtk/window.rs +++ b/druid-shell/src/platform/gtk/window.rs @@ -30,7 +30,7 @@ use gio::ApplicationExt; use gtk::prelude::*; use gtk::{AccelGroup, ApplicationWindow}; -use crate::kurbo::{Point, Size, Vec2}; +use crate::kurbo::{Point, Rect, Size, Vec2}; use crate::piet::{Piet, RenderContext}; use super::application::with_application; @@ -242,14 +242,11 @@ impl WindowBuilder { drawing_area.connect_draw(clone!(handle => move |widget, context| { if let Some(state) = handle.state.upgrade() { - let extents = context.clip_extents(); + let extents = widget.get_allocation(); let dpi_scale = state.window.get_window() .map(|w| w.get_display().get_default_screen().get_resolution()) .unwrap_or(96.0) / 96.0; - let size = ( - ((extents.2 - extents.0) * dpi_scale) as u32, - ((extents.3 - extents.1) * dpi_scale) as u32, - ); + let size = ((extents.width as f64 * dpi_scale) as u32, (extents.height as f64 * dpi_scale) as u32); if last_size.get() != size { last_size.set(size); @@ -258,11 +255,13 @@ impl WindowBuilder { // For some reason piet needs a mutable context, so give it one I guess. let mut context = context.clone(); + let (x0, y0, x1, y1) = context.clip_extents(); let mut piet_context = Piet::new(&mut context); if let Ok(mut handler_borrow) = state.handler.try_borrow_mut() { + let invalid_rect = Rect::new(x0 * dpi_scale, y0 * dpi_scale, x1 * dpi_scale, y1 * dpi_scale); let anim = handler_borrow - .paint(&mut piet_context); + .paint(&mut piet_context, invalid_rect); if let Err(e) = piet_context.finish() { eprintln!("piet error on render: {:?}", e); } @@ -508,6 +507,26 @@ impl WindowHandle { } } + /// Request invalidation of one rectangle. + pub fn invalidate_rect(&self, rect: Rect) { + let dpi_scale = self.get_dpi() as f64 / 96.0; + let rect = Rect::from_origin_size( + (rect.x0 * dpi_scale, rect.y0 * dpi_scale), + rect.size() * dpi_scale, + ); + + // GTK+ takes rects with integer coordinates, and non-negative width/height. + let r = rect.abs().expand(); + if let Some(state) = self.state.upgrade() { + state.window.queue_draw_area( + r.x0 as i32, + r.y0 as i32, + r.width() as i32, + r.height() as i32, + ); + } + } + pub fn text(&self) -> Text { Text::new() } diff --git a/druid-shell/src/platform/mac/window.rs b/druid-shell/src/platform/mac/window.rs index 1e28f8100e..a2f99ce97f 100644 --- a/druid-shell/src/platform/mac/window.rs +++ b/druid-shell/src/platform/mac/window.rs @@ -38,7 +38,7 @@ use objc::{class, msg_send, sel, sel_impl}; use cairo::{Context, QuartzSurface}; use log::{error, info}; -use crate::kurbo::{Point, Size, Vec2}; +use crate::kurbo::{Point, Rect, Size, Vec2}; use crate::piet::{Piet, RenderContext}; use super::dialog; @@ -503,6 +503,10 @@ extern "C" fn draw_rect(this: &mut Object, _: Sel, dirtyRect: NSRect) { let frame = NSView::frame(this as *mut _); let width = frame.size.width as u32; let height = frame.size.height as u32; + let rect = Rect::from_origin_size( + (dirtyRect.origin.x, dirtyRect.origin.y), + (dirtyRect.size.width, dirtyRect.size.height), + ); let cairo_surface = QuartzSurface::create_for_cg_context(cgcontext, width, height).expect("cairo surface"); let mut cairo_ctx = Context::new(&cairo_surface); @@ -511,7 +515,7 @@ extern "C" fn draw_rect(this: &mut Object, _: Sel, dirtyRect: NSRect) { let mut piet_ctx = Piet::new(&mut cairo_ctx); let view_state: *mut c_void = *this.get_ivar("viewState"); let view_state = &mut *(view_state as *mut ViewState); - let anim = (*view_state).handler.paint(&mut piet_ctx); + let anim = (*view_state).handler.paint(&mut piet_ctx, rect); if let Err(e) = piet_ctx.finish() { error!("{}", e) } @@ -639,6 +643,18 @@ impl WindowHandle { } } + /// Request invalidation of one rectangle. + pub fn invalidate_rect(&self, rect: Rect) { + let rect = NSRect::new( + NSPoint::new(rect.x0, rect.y0), + NSSize::new(rect.width(), rect.height()), + ); + unsafe { + // We could share impl with redraw, but we'd need to deal with nil. + let () = msg_send![*self.nsview.load(), setNeedsDisplay: rect]; + } + } + pub fn set_cursor(&mut self, cursor: &Cursor) { unsafe { let nscursor = class!(NSCursor); diff --git a/druid-shell/src/platform/web/window.rs b/druid-shell/src/platform/web/window.rs index 685d979fb3..b3f26a2fdb 100644 --- a/druid-shell/src/platform/web/window.rs +++ b/druid-shell/src/platform/web/window.rs @@ -25,7 +25,7 @@ use instant::Instant; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; -use crate::kurbo::{Point, Size, Vec2}; +use crate::kurbo::{Point, Rect, Size, Vec2}; use crate::piet::RenderContext; @@ -89,14 +89,15 @@ struct WindowState { window: web_sys::Window, canvas: web_sys::HtmlCanvasElement, context: web_sys::CanvasRenderingContext2d, + invalid_rect: Cell, } impl WindowState { - fn render(&self) -> bool { + fn render(&self, rect: Rect) -> bool { self.context .clear_rect(0.0, 0.0, self.get_width() as f64, self.get_height() as f64); let mut piet_ctx = piet_common::Piet::new(self.context.clone(), self.window.clone()); - let want_anim_frame = self.handler.borrow_mut().paint(&mut piet_ctx); + let want_anim_frame = self.handler.borrow_mut().paint(&mut piet_ctx, rect); if let Err(e) = piet_ctx.finish() { log::error!("piet error on render: {:?}", e); } @@ -370,6 +371,7 @@ impl WindowBuilder { window, canvas, context, + invalid_rect: Cell::new(Rect::ZERO), }); setup_web_callbacks(&window); @@ -411,7 +413,27 @@ impl WindowHandle { log::warn!("bring_to_frontand_focus unimplemented for web"); } + pub fn invalidate_rect(&self, rect: Rect) { + if let Some(s) = self.0.upgrade() { + let cur_rect = s.invalid_rect.get(); + if cur_rect.width() == 0.0 || cur_rect.height() == 0.0 { + s.invalid_rect.set(rect); + } else if rect.width() != 0.0 && rect.height() != 0.0 { + s.invalid_rect.set(cur_rect.union(rect)); + } + } + self.render_soon(); + } + pub fn invalidate(&self) { + if let Some(s) = self.0.upgrade() { + let rect = Rect::from_origin_size( + Point::ORIGIN, + // FIXME: does this need scaling? Not sure exactly where dpr enters... + (s.get_width() as f64, s.get_height() as f64), + ); + s.invalid_rect.set(rect); + } self.render_soon(); } @@ -478,9 +500,10 @@ impl WindowHandle { fn render_soon(&self) { if let Some(s) = self.0.upgrade() { let handle = self.clone(); + let rect = s.invalid_rect.get(); let state = s.clone(); s.request_animation_frame(move || { - let want_anim_frame = state.render(); + let want_anim_frame = state.render(rect); if want_anim_frame { handle.render_soon(); } diff --git a/druid-shell/src/platform/windows/window.rs b/druid-shell/src/platform/windows/window.rs index 7e424b96ac..d2d9c3a889 100644 --- a/druid-shell/src/platform/windows/window.rs +++ b/druid-shell/src/platform/windows/window.rs @@ -43,7 +43,7 @@ use piet_common::dwrite::DwriteFactory; use crate::platform::windows::HwndRenderTarget; -use crate::kurbo::{Point, Size, Vec2}; +use crate::kurbo::{Point, Rect, Size, Vec2}; use crate::piet::{Piet, RenderContext}; use super::accels::register_accel; @@ -258,13 +258,19 @@ impl WndState { } // Renders but does not present. - fn render(&mut self, d2d: &D2DFactory, dw: &DwriteFactory, handle: &RefCell) { + fn render( + &mut self, + d2d: &D2DFactory, + dw: &DwriteFactory, + handle: &RefCell, + invalid_rect: Rect, + ) { let rt = self.render_target.as_mut().unwrap(); rt.begin_draw(); let anim; { let mut piet_ctx = Piet::new(d2d, dw, rt); - anim = self.handler.paint(&mut piet_ctx); + anim = self.handler.paint(&mut piet_ctx, invalid_rect); if let Err(e) = piet_ctx.finish() { error!("piet error on render: {:?}", e); } @@ -357,13 +363,20 @@ impl WndProc for MyWndProc { } WM_PAINT => unsafe { if let Ok(mut s) = self.state.try_borrow_mut() { + let mut rect: RECT = mem::zeroed(); + GetUpdateRect(hwnd, &mut rect, 0); let s = s.as_mut().unwrap(); if s.render_target.is_none() { let rt = paint::create_render_target(&self.d2d_factory, hwnd); s.render_target = rt.ok(); } s.handler.rebuild_resources(); - s.render(&self.d2d_factory, &self.dwrite_factory, &self.handle); + s.render( + &self.d2d_factory, + &self.dwrite_factory, + &self.handle, + self.handle.borrow().rect_to_px(rect), + ); if let Some(ref mut ds) = s.dcomp_state { if !ds.sizing { (*ds.swap_chain).Present(1, 0); @@ -380,11 +393,21 @@ impl WndProc for MyWndProc { if let Ok(mut s) = self.state.try_borrow_mut() { let s = s.as_mut().unwrap(); if s.dcomp_state.is_some() { + let mut rect: RECT = mem::zeroed(); + if GetClientRect(hwnd, &mut rect) == 0 { + warn!("GetClientRect failed."); + return None; + } let rt = paint::create_render_target(&self.d2d_factory, hwnd); s.render_target = rt.ok(); { s.handler.rebuild_resources(); - s.render(&self.d2d_factory, &self.dwrite_factory, &self.handle); + s.render( + &self.d2d_factory, + &self.dwrite_factory, + &self.handle, + self.handle.borrow().rect_to_px(rect), + ); } if let Some(ref mut ds) = s.dcomp_state { @@ -419,7 +442,12 @@ impl WndProc for MyWndProc { if SUCCEEDED(res) { s.handler.rebuild_resources(); s.rebuild_render_target(&self.d2d_factory); - s.render(&self.d2d_factory, &self.dwrite_factory, &self.handle); + s.render( + &self.d2d_factory, + &self.dwrite_factory, + &self.handle, + self.handle.borrow().rect_to_px(rect), + ); (*s.dcomp_state.as_ref().unwrap().swap_chain).Present(0, 0); } else { error!("ResizeBuffers failed: 0x{:x}", res); @@ -454,8 +482,6 @@ impl WndProc for MyWndProc { if use_hwnd { if let Some(ref mut rt) = s.render_target { if let Some(hrt) = cast_to_hwnd(rt) { - let width = LOWORD(lparam as u32) as u32; - let height = HIWORD(lparam as u32) as u32; let size = D2D1_SIZE_U { width, height }; let _ = hrt.ptr.Resize(&size); } @@ -474,8 +500,10 @@ impl WndProc for MyWndProc { ); } if SUCCEEDED(res) { + let (w, h) = self.handle.borrow().pixels_to_px_xy(width, height); + let rect = Rect::from_origin_size(Point::ORIGIN, (w as f64, h as f64)); s.rebuild_render_target(&self.d2d_factory); - s.render(&self.d2d_factory, &self.dwrite_factory, &self.handle); + s.render(&self.d2d_factory, &self.dwrite_factory, &self.handle, rect); if let Some(ref mut dcomp_state) = s.dcomp_state { (*dcomp_state.swap_chain).Present(0, 0); let _ = dcomp_state.dcomp_device.commit(); @@ -1171,6 +1199,16 @@ impl WindowHandle { } } + pub fn invalidate_rect(&self, rect: Rect) { + let r = self.px_to_rect(rect); + if let Some(w) = self.state.upgrade() { + let hwnd = w.hwnd.get(); + unsafe { + InvalidateRect(hwnd, &r as *const _, FALSE); + } + } + } + /// Set the title for this menu. pub fn set_title(&self, title: &str) { if let Some(w) = self.state.upgrade() { @@ -1356,6 +1394,23 @@ impl WindowHandle { ((x.into() as f32) * scale, (y.into() as f32) * scale) } + /// Convert a rectangle from physical pixels to px units. + pub fn rect_to_px(&self, rect: RECT) -> Rect { + let (x0, y0) = self.pixels_to_px_xy(rect.left, rect.top); + let (x1, y1) = self.pixels_to_px_xy(rect.right, rect.bottom); + Rect::new(x0 as f64, y0 as f64, x1 as f64, y1 as f64) + } + + pub fn px_to_rect(&self, rect: Rect) -> RECT { + let scale = self.get_dpi() as f64 / 96.0; + RECT { + left: (rect.x0 * scale).floor() as i32, + top: (rect.y0 * scale).floor() as i32, + right: (rect.x1 * scale).ceil() as i32, + bottom: (rect.y1 * scale).ceil() as i32, + } + } + /// Allocate a timer slot. /// /// Returns an id and an elapsed time in ms diff --git a/druid-shell/src/platform/x11/application.rs b/druid-shell/src/platform/x11/application.rs index 15700b46c8..a1def73d34 100644 --- a/druid-shell/src/platform/x11/application.rs +++ b/druid-shell/src/platform/x11/application.rs @@ -23,7 +23,7 @@ use lazy_static::lazy_static; use super::clipboard::Clipboard; use super::window::XWindow; use crate::application::AppHandler; -use crate::kurbo::Point; +use crate::kurbo::{Point, Rect}; use crate::{KeyCode, KeyModifiers, MouseButton, MouseEvent}; struct XcbConnection { @@ -76,10 +76,16 @@ impl Application { xcb::EXPOSE => { let expose: &xcb::ExposeEvent = unsafe { xcb::cast_event(&ev) }; let window_id = expose.window(); + // TODO(x11/dpi_scaling): when dpi scaling is + // implemented, it needs to be used here too + let rect = Rect::from_origin_size( + (expose.x() as f64, expose.y() as f64), + (expose.width() as f64, expose.height() as f64), + ); WINDOW_MAP.with(|map| { let mut windows = map.borrow_mut(); if let Some(w) = windows.get_mut(&window_id) { - w.render(); + w.render(rect); } }) } diff --git a/druid-shell/src/platform/x11/window.rs b/druid-shell/src/platform/x11/window.rs index 38fe8a4df1..06d937f8fb 100644 --- a/druid-shell/src/platform/x11/window.rs +++ b/druid-shell/src/platform/x11/window.rs @@ -22,7 +22,7 @@ use xcb::ffi::XCB_COPY_FROM_PARENT; use crate::dialog::{FileDialogOptions, FileInfo}; use crate::keyboard::{KeyEvent, KeyModifiers}; use crate::keycodes::KeyCode; -use crate::kurbo::{Point, Size}; +use crate::kurbo::{Point, Rect, Size}; use crate::mouse::{Cursor, MouseEvent}; use crate::piet::{Piet, RenderContext}; use crate::window::{IdleToken, Text, TimerToken, WinHandler}; @@ -181,7 +181,7 @@ impl XWindow { xwindow } - pub fn render(&mut self) { + pub fn render(&mut self, invalid_rect: Rect) { let conn = Application::get_connection(); let setup = conn.get_setup(); let screen_num = Application::get_screen_num(); @@ -221,7 +221,7 @@ impl XWindow { cairo_context.set_source_rgb(0.0, 0.0, 0.0); cairo_context.paint(); let mut piet_ctx = Piet::new(&mut cairo_context); - let anim = self.handler.paint(&mut piet_ctx); + let anim = self.handler.paint(&mut piet_ctx, invalid_rect); if let Err(e) = piet_ctx.finish() { // TODO(x11/errors): hook up to error or something? panic!("piet error on render: {:?}", e); @@ -359,6 +359,11 @@ impl WindowHandle { request_redraw(self.window_id); } + pub fn invalidate_rect(&self, _rect: Rect) { + // TODO(x11/render_improvements): set the bounds correctly. + request_redraw(self.window_id); + } + pub fn set_title(&self, title: &str) { let conn = Application::get_connection(); xcb::change_property( diff --git a/druid-shell/src/window.rs b/druid-shell/src/window.rs index 22d1ad94cf..28bb2498db 100644 --- a/druid-shell/src/window.rs +++ b/druid-shell/src/window.rs @@ -21,7 +21,7 @@ use crate::common_util::Counter; use crate::dialog::{FileDialogOptions, FileInfo}; use crate::error::Error; use crate::keyboard::{KeyEvent, KeyModifiers}; -use crate::kurbo::{Point, Size, Vec2}; +use crate::kurbo::{Point, Rect, Size, Vec2}; use crate::menu::Menu; use crate::mouse::{Cursor, MouseEvent}; use crate::platform::window as platform; @@ -131,6 +131,11 @@ impl WindowHandle { self.0.invalidate() } + /// Request invalidation of a region of the window. + pub fn invalidate_rect(&self, rect: Rect) { + self.0.invalidate_rect(rect); + } + /// Set the title for this menu. pub fn set_title(&self, title: &str) { self.0.set_title(title) @@ -278,8 +283,9 @@ pub trait WinHandler { /// Request the handler to paint the window contents. Return value /// indicates whether window is animating, i.e. whether another paint - /// should be scheduled for the next animation frame. - fn paint(&mut self, piet: &mut piet_common::Piet) -> bool; + /// should be scheduled for the next animation frame. `invalid_rect` is the + /// rectangle that needs to be repainted. + fn paint(&mut self, piet: &mut piet_common::Piet, invalid_rect: Rect) -> bool; /// Called when the resources need to be rebuilt. /// diff --git a/druid/examples/invalidation.rs b/druid/examples/invalidation.rs new file mode 100644 index 0000000000..bf8ebbd0c8 --- /dev/null +++ b/druid/examples/invalidation.rs @@ -0,0 +1,88 @@ +// Copyright 2019 The xi-editor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Demonstrates how to debug invalidation regions, and also shows the +//! invalidation behavior of several build-in widgets. + +use druid::kurbo::{Circle, Shape}; +use druid::widget::prelude::*; +use druid::widget::{Button, Flex, Scroll, Split, TextBox}; +use druid::{AppLauncher, Color, Data, Lens, LocalizedString, Point, WidgetExt, WindowDesc}; + +pub fn main() { + let window = WindowDesc::new(build_widget).title( + LocalizedString::new("invalidate-demo-window-title").with_placeholder("Invalidate demo"), + ); + let state = AppState { + label: "My label".into(), + circle_pos: Point::new(0.0, 0.0), + }; + AppLauncher::with_window(window) + .use_simple_logger() + .launch(state) + .expect("launch failed"); +} + +#[derive(Clone, Data, Lens)] +struct AppState { + label: String, + circle_pos: Point, +} + +fn build_widget() -> impl Widget { + let mut col = Flex::column(); + col.add_child(TextBox::new().lens(AppState::label).padding(3.0)); + for i in 0..30 { + col.add_child(Button::new(format!("Button {}", i)).padding(3.0)); + } + Split::columns(Scroll::new(col), CircleView.lens(AppState::circle_pos)).debug_invalidation() +} + +struct CircleView; + +const RADIUS: f64 = 25.0; + +impl Widget for CircleView { + fn event(&mut self, ctx: &mut EventCtx, ev: &Event, data: &mut Point, _env: &Env) { + if let Event::MouseDown(ev) = ev { + // Move the circle to a new location, invalidating both the old and new locations. + ctx.request_paint_rect(Circle::new(*data, RADIUS).bounding_box()); + ctx.request_paint_rect(Circle::new(ev.pos, RADIUS).bounding_box()); + *data = ev.pos; + } + } + + fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, _ev: &LifeCycle, _data: &Point, _env: &Env) {} + + fn update(&mut self, _ctx: &mut UpdateCtx, _old: &Point, _new: &Point, _env: &Env) {} + + fn layout( + &mut self, + _ctx: &mut LayoutCtx, + bc: &BoxConstraints, + _data: &Point, + _env: &Env, + ) -> Size { + bc.max() + } + + fn paint(&mut self, ctx: &mut PaintCtx, data: &Point, _env: &Env) { + ctx.with_save(|ctx| { + let rect = ctx.size().to_rect(); + ctx.clip(rect); + ctx.fill(rect, &Color::WHITE); + ctx.fill(Circle::new(*data, RADIUS), &Color::BLACK); + }) + } +} diff --git a/druid/examples/wasm/src/lib.rs b/druid/examples/wasm/src/lib.rs index d2ea7b4041..4db2d14657 100644 --- a/druid/examples/wasm/src/lib.rs +++ b/druid/examples/wasm/src/lib.rs @@ -45,6 +45,7 @@ impl_example!(game_of_life); impl_example!(hello); impl_example!(identity); impl_example!(image); +impl_example!(invalidation); impl_example!(layout); impl_example!(lens); impl_example!(list); diff --git a/druid/src/contexts.rs b/druid/src/contexts.rs index f59d5d1688..78b0c59cd8 100644 --- a/druid/src/contexts.rs +++ b/druid/src/contexts.rs @@ -21,7 +21,7 @@ use crate::core::{BaseState, CommandQueue, FocusChange}; use crate::piet::Piet; use crate::piet::RenderContext; use crate::{ - Affine, Command, Cursor, Insets, Point, Rect, Size, Target, Text, TimerToken, WidgetId, + Affine, Command, Cursor, Insets, Point, Rect, Size, Target, Text, TimerToken, Vec2, WidgetId, WindowHandle, WindowId, }; @@ -117,25 +117,38 @@ pub struct PaintCtx<'a, 'b: 'a> { } /// A region of a widget, generally used to describe what needs to be drawn. +/// +/// This is currently just a single `Rect`, but may become more complicated in the future. Although +/// this is just a wrapper around `Rect`, it has some different conventions. Mainly, "signed" +/// invalidation regions don't make sense. Therefore, a rectangle with non-positive width or height +/// is considered "empty", and all empty rectangles are treated the same. #[derive(Debug, Clone)] pub struct Region(Rect); impl<'a> EventCtx<'a> { #[deprecated(since = "0.5.0", note = "use request_paint instead")] pub fn invalidate(&mut self) { - // Note: for the current functionality, we could shortcut and just - // request an invalidate on the window. But when we do fine-grained - // invalidation, we'll want to compute the invalidation region, and - // that needs to be propagated (with, likely, special handling for - // scrolling). - self.base_state.needs_inval = true; + self.request_paint(); } - /// Request a [`paint`] pass. + /// Request a [`paint`] pass. This is equivalent to calling [`request_paint_rect`] for the + /// widget's [`paint_rect`]. /// /// [`paint`]: trait.Widget.html#tymethod.paint + /// [`request_paint_rect`]: struct.EventCtx.html#method.request_paint_rect + /// [`paint_rect`]: struct.WidgetPod.html#method.paint_rect pub fn request_paint(&mut self) { - self.base_state.needs_inval = true; + self.request_paint_rect( + self.base_state.paint_rect() - self.base_state.layout_rect().origin().to_vec2(), + ); + } + + /// Request a [`paint`] pass for redrawing a rectangle, which is given relative to our layout + /// rectangle. + /// + /// [`paint`]: trait.Widget.html#tymethod.paint + pub fn request_paint_rect(&mut self, rect: Rect) { + self.base_state.invalid.add_rect(rect); } /// Request a layout pass. @@ -150,7 +163,6 @@ impl<'a> EventCtx<'a> { /// [`layout`]: trait.Widget.html#tymethod.layout pub fn request_layout(&mut self) { self.base_state.needs_layout = true; - self.base_state.needs_inval = true; } /// Indicate that your children have changed. @@ -158,8 +170,7 @@ impl<'a> EventCtx<'a> { /// Widgets must call this method after adding a new child. pub fn children_changed(&mut self) { self.base_state.children_changed = true; - self.base_state.needs_layout = true; - self.base_state.needs_inval = true; + self.request_layout(); } /// Get an object which can create text layouts. @@ -337,7 +348,7 @@ impl<'a> EventCtx<'a> { /// Request an animation frame. pub fn request_anim_frame(&mut self) { self.base_state.request_anim = true; - self.base_state.needs_inval = true; + self.request_paint(); } /// Request a timer event. @@ -393,14 +404,27 @@ impl<'a> EventCtx<'a> { impl<'a> LifeCycleCtx<'a> { #[deprecated(since = "0.5.0", note = "use request_paint instead")] pub fn invalidate(&mut self) { - self.base_state.needs_inval = true; + self.request_paint(); } - /// Request a [`paint`] pass. + /// Request a [`paint`] pass. This is equivalent to calling [`request_paint_rect`] for the + /// widget's [`paint_rect`]. /// /// [`paint`]: trait.Widget.html#tymethod.paint + /// [`request_paint_rect`]: struct.LifeCycleCtx.html#method.request_paint_rect + /// [`paint_rect`]: struct.WidgetPod.html#method.paint_rect pub fn request_paint(&mut self) { - self.base_state.needs_inval = true; + self.request_paint_rect( + self.base_state.paint_rect() - self.base_state.layout_rect().origin().to_vec2(), + ); + } + + /// Request a [`paint`] pass for redrawing a rectangle, which is given relative to our layout + /// rectangle. + /// + /// [`paint`]: trait.Widget.html#tymethod.paint + pub fn request_paint_rect(&mut self, rect: Rect) { + self.base_state.invalid.add_rect(rect); } /// Request layout. @@ -410,7 +434,6 @@ impl<'a> LifeCycleCtx<'a> { /// [`EventCtx::request_layout`]: struct.EventCtx.html#method.request_layout pub fn request_layout(&mut self) { self.base_state.needs_layout = true; - self.base_state.needs_inval = true; } /// Returns the current widget's `WidgetId`. @@ -445,8 +468,7 @@ impl<'a> LifeCycleCtx<'a> { /// Widgets must call this method after adding a new child. pub fn children_changed(&mut self) { self.base_state.children_changed = true; - self.base_state.needs_layout = true; - self.base_state.needs_inval = true; + self.request_layout(); } /// Request an animation frame. @@ -475,14 +497,27 @@ impl<'a> LifeCycleCtx<'a> { impl<'a> UpdateCtx<'a> { #[deprecated(since = "0.5.0", note = "use request_paint instead")] pub fn invalidate(&mut self) { - self.base_state.needs_inval = true; + self.request_paint(); } - /// Request a [`paint`] pass. + /// Request a [`paint`] pass. This is equivalent to calling [`request_paint_rect`] for the + /// widget's [`paint_rect`]. /// /// [`paint`]: trait.Widget.html#tymethod.paint + /// [`request_paint_rect`]: struct.UpdateCtx.html#method.request_paint_rect + /// [`paint_rect`]: struct.WidgetPod.html#method.paint_rect pub fn request_paint(&mut self) { - self.base_state.needs_inval = true; + self.request_paint_rect( + self.base_state.paint_rect() - self.base_state.layout_rect().origin().to_vec2(), + ); + } + + /// Request a [`paint`] pass for redrawing a rectangle, which is given relative to our layout + /// rectangle. + /// + /// [`paint`]: trait.Widget.html#tymethod.paint + pub fn request_paint_rect(&mut self, rect: Rect) { + self.base_state.invalid.add_rect(rect); } /// Request layout. @@ -492,7 +527,6 @@ impl<'a> UpdateCtx<'a> { /// [`EventCtx::request_layout`]: struct.EventCtx.html#method.request_layout pub fn request_layout(&mut self) { self.base_state.needs_layout = true; - self.base_state.needs_inval = true; } /// Indicate that your children have changed. @@ -500,8 +534,7 @@ impl<'a> UpdateCtx<'a> { /// Widgets must call this method after adding a new child. pub fn children_changed(&mut self) { self.base_state.children_changed = true; - self.base_state.needs_layout = true; - self.base_state.needs_inval = true; + self.request_layout(); } /// Submit a [`Command`] to be run after layout and paint finish. @@ -704,6 +737,9 @@ impl<'a, 'b: 'a> PaintCtx<'a, 'b> { } impl Region { + /// An empty region. + pub const EMPTY: Region = Region(Rect::ZERO); + /// Returns the smallest `Rect` that encloses the entire region. pub fn to_rect(&self) -> Rect { self.0 @@ -714,11 +750,51 @@ impl Region { pub fn intersects(&self, other: Rect) -> bool { self.0.intersect(other).area() > 0. } + + /// Returns `true` if this region is empty. + pub fn is_empty(&self) -> bool { + self.0.width() <= 0.0 || self.0.height() <= 0.0 + } + + /// Adds a new `Rect` to this region. + /// + /// This differs from `Rect::union` in its treatment of empty rectangles: an empty rectangle has + /// no effect on the union. + pub(crate) fn add_rect(&mut self, rect: Rect) { + if self.is_empty() { + self.0 = rect; + } else if rect.width() > 0.0 && rect.height() > 0.0 { + self.0 = self.0.union(rect); + } + } + + /// Modifies this region by including everything in the other region. + pub(crate) fn merge_with(&mut self, other: Region) { + self.add_rect(other.0); + } + + /// Modifies this region by intersecting it with the given rectangle. + pub(crate) fn intersect_with(&mut self, rect: Rect) { + self.0 = self.0.intersect(rect); + } +} + +impl std::ops::AddAssign for Region { + fn add_assign(&mut self, offset: Vec2) { + self.0 = self.0 + offset; + } +} + +impl std::ops::SubAssign for Region { + fn sub_assign(&mut self, offset: Vec2) { + self.0 = self.0 - offset; + } } impl From for Region { fn from(src: Rect) -> Region { - Region(src) + // We maintain the invariant that the width/height of the rect are non-negative. + Region(src.abs()) } } diff --git a/druid/src/core.rs b/druid/src/core.rs index 79140ea5a5..c1d191cc25 100644 --- a/druid/src/core.rs +++ b/druid/src/core.rs @@ -19,11 +19,12 @@ use std::collections::VecDeque; use log; use crate::bloom::Bloom; -use crate::kurbo::{Affine, Insets, Point, Rect, Shape, Size}; +use crate::kurbo::{Affine, Insets, Point, Rect, Shape, Size, Vec2}; use crate::piet::RenderContext; use crate::{ BoxConstraints, Command, Data, Env, Event, EventCtx, InternalEvent, InternalLifeCycle, - LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Target, UpdateCtx, Widget, WidgetId, WindowId, + LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Region, Target, UpdateCtx, Widget, WidgetId, + WindowId, }; /// Our queue type @@ -75,11 +76,15 @@ pub(crate) struct BaseState { /// drop shadows or overflowing text. pub(crate) paint_insets: Insets, - // TODO: consider using bitflags for the booleans. + // The region that needs to be repainted, relative to the widget's bounds. + pub(crate) invalid: Region, - // This should become an invalidation rect. - pub(crate) needs_inval: bool, + // The part of this widget that is visible on the screen is offset by this + // much. This will be non-zero for widgets that are children of `Scroll`, or + // similar, and it is used for propagating invalid regions. + pub(crate) viewport_offset: Vec2, + // TODO: consider using bitflags for the booleans. pub(crate) is_hot: bool, pub(crate) is_active: bool, @@ -212,6 +217,28 @@ impl> WidgetPod { self.state.layout_rect.unwrap_or_default() } + /// Set the viewport offset. + /// + /// This is relevant only for children of a scroll view (or similar). It must + /// be set by the parent widget whenever it modifies the position of its child + /// while painting it and propagating events. As a rule of thumb, you need this + /// if and only if you `Affine::translate` the paint context before painting + /// your child. For an example, see the implentation of [`Scroll`]. + /// + /// [`Scroll`]: widget/struct.Scroll.html + pub fn set_viewport_offset(&mut self, offset: Vec2) { + self.state.viewport_offset = offset; + } + + /// The viewport offset. + /// + /// This will be the same value as set by [`set_viewport_offset`]. + /// + /// [`set_viewport_offset`]: #method.viewport_offset + pub fn viewport_offset(&self) -> Vec2 { + self.state.viewport_offset + } + /// Get the widget's paint [`Rect`]. /// /// This is the [`Rect`] that widget has indicated it needs to paint in. @@ -308,6 +335,9 @@ impl> WidgetPod { /// [`paint`]: trait.Widget.html#tymethod.paint /// [`paint_with_offset`]: #method.paint_with_offset pub fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { + let mut child_region = ctx.region.clone(); + child_region -= self.state.layout_rect().origin().to_vec2(); + child_region.intersect_with(self.state.paint_rect()); let mut inner_ctx = PaintCtx { render_ctx: ctx.render_ctx, window_id: ctx.window_id, @@ -327,7 +357,7 @@ impl> WidgetPod { inner_ctx.stroke(rect, &color, BORDER_WIDTH); } - self.state.needs_inval = false; + self.state.invalid = Region::EMPTY; } /// Paint the widget, translating it by the origin of its layout rectangle. @@ -742,7 +772,8 @@ impl BaseState { id, layout_rect: None, paint_insets: Insets::ZERO, - needs_inval: false, + invalid: Region::EMPTY, + viewport_offset: Vec2::ZERO, is_hot: false, needs_layout: false, is_active: false, @@ -759,7 +790,15 @@ impl BaseState { /// Update to incorporate state changes from a child. fn merge_up(&mut self, child_state: &BaseState) { - self.needs_inval |= child_state.needs_inval; + let mut child_region = child_state.invalid.clone(); + child_region += child_state.layout_rect().origin().to_vec2() - child_state.viewport_offset; + let clip = self + .layout_rect() + .with_origin(Point::ORIGIN) + .inset(self.paint_insets); + child_region.intersect_with(clip); + self.invalid.merge_with(child_region); + self.needs_layout |= child_state.needs_layout; self.request_anim |= child_state.request_anim; self.request_timer |= child_state.request_timer; @@ -783,8 +822,6 @@ impl BaseState { self.layout_rect.unwrap_or_default() + self.paint_insets } - #[cfg(test)] - #[allow(dead_code)] pub(crate) fn layout_rect(&self) -> Rect { self.layout_rect.unwrap_or_default() } diff --git a/druid/src/tests/harness.rs b/druid/src/tests/harness.rs index 27016c3c4a..1c06cf3bde 100644 --- a/druid/src/tests/harness.rs +++ b/druid/src/tests/harness.rs @@ -263,9 +263,13 @@ impl Harness<'_, T> { self.inner.layout(&mut self.piet) } + pub fn paint_rect(&mut self, invalid_rect: Rect) { + self.inner.paint_rect(&mut self.piet, invalid_rect) + } + #[allow(dead_code)] pub fn paint(&mut self) { - self.inner.paint(&mut self.piet) + self.paint_rect(self.window_size.to_rect()) } } @@ -290,9 +294,9 @@ impl Inner { } #[allow(dead_code)] - fn paint(&mut self, piet: &mut Piet) { + fn paint_rect(&mut self, piet: &mut Piet, invalid_rect: Rect) { self.window - .do_paint(piet, &mut self.cmds, &self.data, &self.env); + .do_paint(piet, invalid_rect, &mut self.cmds, &self.data, &self.env); } } diff --git a/druid/src/tests/helpers.rs b/druid/src/tests/helpers.rs index 348e697dc6..19d382fdbb 100644 --- a/druid/src/tests/helpers.rs +++ b/druid/src/tests/helpers.rs @@ -91,7 +91,7 @@ pub enum Record { /// A `LifeCycle` event. L(LifeCycle), Layout(Size), - Update(bool), + Update(Rect), Paint, // instead of always returning an Option, we have a none variant; // this would be code smell elsewhere but here I think it makes the tests @@ -289,8 +289,8 @@ impl> Widget for Recorder { fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &Env) { self.inner.update(ctx, old_data, data, env); - let inval = ctx.base_state.needs_inval; - self.recording.push(Record::Update(inval)); + self.recording + .push(Record::Update(ctx.base_state.invalid.to_rect())); } fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { diff --git a/druid/src/widget/invalidation.rs b/druid/src/widget/invalidation.rs new file mode 100644 index 0000000000..014fed6797 --- /dev/null +++ b/druid/src/widget/invalidation.rs @@ -0,0 +1,66 @@ +// Copyright 2020 The xi-editor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::widget::prelude::*; +use crate::Data; + +/// A widget that draws semi-transparent rectangles of changing colors to help debug invalidation +/// regions. +pub struct DebugInvalidation { + inner: W, + debug_color: u64, + marker: std::marker::PhantomData, +} + +impl> DebugInvalidation { + /// Wraps a widget in a `DebugInvalidation`. + pub fn new(inner: W) -> Self { + Self { + inner, + debug_color: 0, + marker: std::marker::PhantomData, + } + } +} + +impl> Widget for DebugInvalidation { + fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { + self.inner.event(ctx, event, data, env); + } + + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { + self.inner.lifecycle(ctx, event, data, env) + } + + fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &Env) { + self.inner.update(ctx, old_data, data, env); + } + + fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { + self.inner.layout(ctx, bc, data, env) + } + + fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { + self.inner.paint(ctx, data, env); + + let color = env.get_debug_color(self.debug_color).with_alpha(0.5); + let rect = ctx.region().to_rect(); + ctx.fill(rect, &color); + self.debug_color += 1; + } + + fn id(&self) -> Option { + self.inner.id() + } +} diff --git a/druid/src/widget/mod.rs b/druid/src/widget/mod.rs index ee0ed25a14..15fca2c885 100644 --- a/druid/src/widget/mod.rs +++ b/druid/src/widget/mod.rs @@ -28,6 +28,7 @@ mod identity_wrapper; #[cfg(feature = "image")] #[cfg_attr(docsrs, doc(cfg(feature = "image")))] mod image; +mod invalidation; mod label; mod list; mod padding; @@ -64,6 +65,7 @@ pub use either::Either; pub use env_scope::EnvScope; pub use flex::{CrossAxisAlignment, Flex, FlexParams, MainAxisAlignment}; pub use identity_wrapper::IdentityWrapper; +pub use invalidation::DebugInvalidation; pub use label::{Label, LabelText}; pub use list::{List, ListIter}; pub use padding::Padding; diff --git a/druid/src/widget/scroll.rs b/druid/src/widget/scroll.rs index 44c5e6755e..fda6748cbf 100644 --- a/druid/src/widget/scroll.rs +++ b/druid/src/widget/scroll.rs @@ -163,6 +163,7 @@ impl> Scroll { offset.y = offset.y.min(self.child_size.height - size.height).max(0.0); if (offset - self.scroll_offset).hypot2() > 1e-12 { self.scroll_offset = offset; + self.child.set_viewport_offset(offset); true } else { false @@ -381,7 +382,7 @@ impl> Widget for Scroll { let force_event = self.child.is_hot() || self.child.is_active(); let child_event = event.transform_scroll(self.scroll_offset, viewport, force_event); if let Some(child_event) = child_event { - self.child.event(ctx, &child_event, data, env) + self.child.event(ctx, &child_event, data, env); }; match event { diff --git a/druid/src/widget/widget_ext.rs b/druid/src/widget/widget_ext.rs index ce697c795e..942eeef8d3 100644 --- a/druid/src/widget/widget_ext.rs +++ b/druid/src/widget/widget_ext.rs @@ -15,8 +15,8 @@ //! Convenience methods for widgets. use super::{ - Align, BackgroundBrush, Click, Container, Controller, ControllerHost, EnvScope, - IdentityWrapper, Padding, Parse, SizedBox, WidgetId, + Align, BackgroundBrush, Click, Container, Controller, ControllerHost, DebugInvalidation, + EnvScope, IdentityWrapper, Padding, Parse, SizedBox, WidgetId, }; use crate::{Color, Data, Env, EventCtx, Insets, KeyOrValue, Lens, LensWrap, UnitPoint, Widget}; @@ -181,6 +181,12 @@ pub trait WidgetExt: Widget + Sized + 'static { EnvScope::new(|env, _| env.set(Env::DEBUG_PAINT, true), self) } + /// Draw a color-changing rectangle over this widget, allowing you to see the + /// invalidation regions. + fn debug_invalidation(self) -> DebugInvalidation { + DebugInvalidation::new(self) + } + /// Set the [`DEBUG_WIDGET`] env variable for this widget (and its descendants). /// /// This does nothing by default, but you can use this variable while diff --git a/druid/src/win_handler.rs b/druid/src/win_handler.rs index 5fc65839dc..bc2e5c2bb8 100644 --- a/druid/src/win_handler.rs +++ b/druid/src/win_handler.rs @@ -19,7 +19,7 @@ use std::cell::RefCell; use std::collections::{HashMap, VecDeque}; use std::rc::Rc; -use crate::kurbo::{Size, Vec2}; +use crate::kurbo::{Rect, Size, Vec2}; use crate::piet::Piet; use crate::shell::{ Application, FileDialogOptions, IdleToken, MouseEvent, WinHandler, WindowHandle, @@ -269,9 +269,9 @@ impl Inner { } /// Returns `true` if an animation frame was requested. - fn paint(&mut self, window_id: WindowId, piet: &mut Piet) -> bool { + fn paint(&mut self, window_id: WindowId, piet: &mut Piet, rect: Rect) -> bool { if let Some(win) = self.windows.get_mut(window_id) { - win.do_paint(piet, &mut self.command_queue, &self.data, &self.env); + win.do_paint(piet, rect, &mut self.command_queue, &self.data, &self.env); win.wants_animation_frame() } else { false @@ -436,8 +436,8 @@ impl AppState { result } - fn paint_window(&mut self, window_id: WindowId, piet: &mut Piet) -> bool { - self.inner.borrow_mut().paint(window_id, piet) + fn paint_window(&mut self, window_id: WindowId, piet: &mut Piet, rect: Rect) -> bool { + self.inner.borrow_mut().paint(window_id, piet, rect) } fn idle(&mut self, token: IdleToken) { @@ -611,8 +611,8 @@ impl WinHandler for DruidHandler { self.app_state.do_window_event(event, self.window_id); } - fn paint(&mut self, piet: &mut Piet) -> bool { - self.app_state.paint_window(self.window_id, piet) + fn paint(&mut self, piet: &mut Piet, rect: Rect) -> bool { + self.app_state.paint_window(self.window_id, piet, rect) } fn size(&mut self, width: u32, height: u32) { diff --git a/druid/src/window.rs b/druid/src/window.rs index 73223206db..546b132f18 100644 --- a/druid/src/window.rs +++ b/druid/src/window.rs @@ -274,8 +274,13 @@ impl Window { } pub(crate) fn invalidate_and_finalize(&mut self) { - if self.root.state().needs_inval { + if self.root.state().needs_layout { self.handle.invalidate(); + } else { + let invalid = &self.root.state().invalid; + if !invalid.is_empty() { + self.handle.invalidate_rect(invalid.to_rect()); + } } } @@ -284,6 +289,7 @@ impl Window { pub(crate) fn do_paint( &mut self, piet: &mut Piet, + invalid_rect: Rect, queue: &mut CommandQueue, data: &T, env: &Env, @@ -296,7 +302,7 @@ impl Window { } piet.clear(env.get(crate::theme::WINDOW_BACKGROUND_COLOR)); - self.paint(piet, data, env); + self.paint(piet, invalid_rect, data, env); } fn layout(&mut self, piet: &mut Piet, queue: &mut CommandQueue, data: &T, env: &Env) { @@ -332,7 +338,7 @@ impl Window { self.layout(piet, queue, data, env) } - fn paint(&mut self, piet: &mut Piet, data: &T, env: &Env) { + fn paint(&mut self, piet: &mut Piet, invalid_rect: Rect, data: &T, env: &Env) { let base_state = BaseState::new(self.root.id()); let mut ctx = PaintCtx { render_ctx: piet, @@ -340,7 +346,7 @@ impl Window { window_id: self.id, z_ops: Vec::new(), focus_widget: self.focus, - region: Rect::ZERO.into(), + region: invalid_rect.into(), }; let visible = Rect::from_origin_size(Point::ZERO, self.size); ctx.with_child_ctx(visible, |ctx| self.root.paint(ctx, data, env)); From 5adab1362dc70cfb6834872616cd03ea0d266b03 Mon Sep 17 00:00:00 2001 From: Joe Neeman Date: Tue, 21 Apr 2020 14:25:16 -0500 Subject: [PATCH 2/7] Fix the small issues from code review. --- druid-shell/examples/invalidate.rs | 15 +++------------ druid/src/widget/mod.rs | 1 - druid/src/widget/widget_ext.rs | 5 +++-- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/druid-shell/examples/invalidate.rs b/druid-shell/examples/invalidate.rs index 5497127541..83d347fd19 100644 --- a/druid-shell/examples/invalidate.rs +++ b/druid-shell/examples/invalidate.rs @@ -16,8 +16,8 @@ use std::any::Any; use std::time::Instant; -use piet_common::kurbo::{Point, Rect}; -use piet_common::{Color, Piet, RenderContext}; +use druid_shell::kurbo::{Point, Rect}; +use druid_shell::piet::{Color, Piet, RenderContext}; use druid_shell::{Application, WinHandler, WindowBuilder, WindowHandle}; @@ -29,19 +29,10 @@ struct InvalidateTest { rect: Rect, } -fn split_rgb(c: &Color) -> (u8, u8, u8) { - let rgba = c.as_rgba_u32(); - ( - (rgba >> 24 & 255) as u8, - (rgba >> 16 & 255) as u8, - (rgba >> 8 & 255) as u8, - ) -} - impl InvalidateTest { fn update_color_and_rect(&mut self) { let time_since_start = (Instant::now() - self.start_time).as_nanos(); - let (r, g, b) = split_rgb(&self.color); + let (r, g, b, _) = self.color.as_rgba_u8(); self.color = match (time_since_start % 2, time_since_start % 3) { (0, _) => Color::rgb8(r.wrapping_add(10), g, b), (_, 0) => Color::rgb8(r, g.wrapping_add(10), b), diff --git a/druid/src/widget/mod.rs b/druid/src/widget/mod.rs index 15fca2c885..ce5c9aa7ec 100644 --- a/druid/src/widget/mod.rs +++ b/druid/src/widget/mod.rs @@ -65,7 +65,6 @@ pub use either::Either; pub use env_scope::EnvScope; pub use flex::{CrossAxisAlignment, Flex, FlexParams, MainAxisAlignment}; pub use identity_wrapper::IdentityWrapper; -pub use invalidation::DebugInvalidation; pub use label::{Label, LabelText}; pub use list::{List, ListIter}; pub use padding::Padding; diff --git a/druid/src/widget/widget_ext.rs b/druid/src/widget/widget_ext.rs index 942eeef8d3..41b432d799 100644 --- a/druid/src/widget/widget_ext.rs +++ b/druid/src/widget/widget_ext.rs @@ -14,9 +14,10 @@ //! Convenience methods for widgets. +use super::invalidation::DebugInvalidation; use super::{ - Align, BackgroundBrush, Click, Container, Controller, ControllerHost, DebugInvalidation, - EnvScope, IdentityWrapper, Padding, Parse, SizedBox, WidgetId, + Align, BackgroundBrush, Click, Container, Controller, ControllerHost, EnvScope, + IdentityWrapper, Padding, Parse, SizedBox, WidgetId, }; use crate::{Color, Data, Env, EventCtx, Insets, KeyOrValue, Lens, LensWrap, UnitPoint, Widget}; From efd652147ca13e281ead6671d408bc3ab2c8ed6c Mon Sep 17 00:00:00 2001 From: Joe Neeman Date: Tue, 21 Apr 2020 17:15:30 -0500 Subject: [PATCH 3/7] Fix propagation of invalid region to children. --- druid/src/core.rs | 5 +---- druid/src/widget/scroll.rs | 2 +- druid/src/window.rs | 5 ++--- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/druid/src/core.rs b/druid/src/core.rs index c1d191cc25..d3c775494e 100644 --- a/druid/src/core.rs +++ b/druid/src/core.rs @@ -335,9 +335,6 @@ impl> WidgetPod { /// [`paint`]: trait.Widget.html#tymethod.paint /// [`paint_with_offset`]: #method.paint_with_offset pub fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { - let mut child_region = ctx.region.clone(); - child_region -= self.state.layout_rect().origin().to_vec2(); - child_region.intersect_with(self.state.paint_rect()); let mut inner_ctx = PaintCtx { render_ctx: ctx.render_ctx, window_id: ctx.window_id, @@ -390,7 +387,7 @@ impl> WidgetPod { ctx.with_save(|ctx| { let layout_origin = self.layout_rect().origin().to_vec2(); ctx.transform(Affine::translate(layout_origin)); - let visible = ctx.region().to_rect() - layout_origin; + let visible = ctx.region().to_rect().intersect(self.state.paint_rect()) - layout_origin; ctx.with_child_ctx(visible, |ctx| self.paint(ctx, data, env)); }); } diff --git a/druid/src/widget/scroll.rs b/druid/src/widget/scroll.rs index fda6748cbf..36aede6070 100644 --- a/druid/src/widget/scroll.rs +++ b/druid/src/widget/scroll.rs @@ -453,7 +453,7 @@ impl> Widget for Scroll { ctx.clip(viewport); ctx.transform(Affine::translate(-self.scroll_offset)); - let visible = viewport.with_origin(self.scroll_offset.to_point()); + let visible = ctx.region().to_rect() + self.scroll_offset; ctx.with_child_ctx(visible, |ctx| self.child.paint(ctx, data, env)); self.draw_bars(ctx, viewport, env); diff --git a/druid/src/window.rs b/druid/src/window.rs index 546b132f18..a8ed68fc5b 100644 --- a/druid/src/window.rs +++ b/druid/src/window.rs @@ -348,14 +348,13 @@ impl Window { focus_widget: self.focus, region: invalid_rect.into(), }; - let visible = Rect::from_origin_size(Point::ZERO, self.size); - ctx.with_child_ctx(visible, |ctx| self.root.paint(ctx, data, env)); + ctx.with_child_ctx(invalid_rect, |ctx| self.root.paint(ctx, data, env)); let mut z_ops = mem::take(&mut ctx.z_ops); z_ops.sort_by_key(|k| k.z_index); for z_op in z_ops.into_iter() { - ctx.with_child_ctx(visible, |ctx| { + ctx.with_child_ctx(invalid_rect, |ctx| { ctx.with_save(|ctx| { ctx.render_ctx.transform(z_op.transform); (z_op.paint_func)(ctx); From 8959bc571ccf4554ea0e0418b27f62d7892e3616 Mon Sep 17 00:00:00 2001 From: Joe Neeman Date: Wed, 22 Apr 2020 10:15:00 -0500 Subject: [PATCH 4/7] Hopefully fix invalidation on mac. --- druid-shell/examples/invalidate.rs | 14 +++++++++----- druid-shell/src/platform/mac/window.rs | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/druid-shell/examples/invalidate.rs b/druid-shell/examples/invalidate.rs index 83d347fd19..2e4277083a 100644 --- a/druid-shell/examples/invalidate.rs +++ b/druid-shell/examples/invalidate.rs @@ -14,12 +14,12 @@ use std::any::Any; -use std::time::Instant; +use std::time::{Duration, Instant}; use druid_shell::kurbo::{Point, Rect}; use druid_shell::piet::{Color, Piet, RenderContext}; -use druid_shell::{Application, WinHandler, WindowBuilder, WindowHandle}; +use druid_shell::{Application, TimerToken, WinHandler, WindowBuilder, WindowHandle}; struct InvalidateTest { handle: WindowHandle, @@ -49,13 +49,17 @@ impl InvalidateTest { impl WinHandler for InvalidateTest { fn connect(&mut self, handle: &WindowHandle) { self.handle = handle.clone(); + self.handle.request_timer(Duration::from_millis(60)); } - fn paint(&mut self, piet: &mut Piet, rect: Rect) -> bool { + fn timer(&mut self, _id: TimerToken) { self.update_color_and_rect(); - piet.fill(rect, &self.color); - self.handle.invalidate_rect(self.rect); + self.handle.request_timer(Duration::from_millis(60)); + } + + fn paint(&mut self, piet: &mut Piet, rect: Rect) -> bool { + piet.fill(rect, &self.color); false } diff --git a/druid-shell/src/platform/mac/window.rs b/druid-shell/src/platform/mac/window.rs index a2f99ce97f..9dcd5f30c1 100644 --- a/druid-shell/src/platform/mac/window.rs +++ b/druid-shell/src/platform/mac/window.rs @@ -651,7 +651,7 @@ impl WindowHandle { ); unsafe { // We could share impl with redraw, but we'd need to deal with nil. - let () = msg_send![*self.nsview.load(), setNeedsDisplay: rect]; + let () = msg_send![*self.nsview.load(), setNeedsDisplayInRect: rect]; } } From fed49b13b3f488370f90ea559535f540a9ab28c0 Mon Sep 17 00:00:00 2001 From: Joe Neeman Date: Wed, 22 Apr 2020 12:18:11 -0500 Subject: [PATCH 5/7] Use rectangle outlines for debug_invalidation. --- druid/src/widget/invalidation.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/druid/src/widget/invalidation.rs b/druid/src/widget/invalidation.rs index 014fed6797..cdd6bddca8 100644 --- a/druid/src/widget/invalidation.rs +++ b/druid/src/widget/invalidation.rs @@ -54,9 +54,10 @@ impl> Widget for DebugInvalidation { fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { self.inner.paint(ctx, data, env); - let color = env.get_debug_color(self.debug_color).with_alpha(0.5); - let rect = ctx.region().to_rect(); - ctx.fill(rect, &color); + let color = env.get_debug_color(self.debug_color); + let stroke_width = 2.0; + let rect = ctx.region().to_rect().inset(-stroke_width / 2.0); + ctx.stroke(rect, &color, stroke_width); self.debug_color += 1; } From 0f8b861e5790093c204477e3dfac0fadec726153 Mon Sep 17 00:00:00 2001 From: Joe Neeman Date: Wed, 22 Apr 2020 15:22:18 -0500 Subject: [PATCH 6/7] Propagate the invalid rects properly in wasm. --- druid-shell/src/platform/web/window.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/druid-shell/src/platform/web/window.rs b/druid-shell/src/platform/web/window.rs index b3f26a2fdb..a84abdbced 100644 --- a/druid-shell/src/platform/web/window.rs +++ b/druid-shell/src/platform/web/window.rs @@ -93,11 +93,9 @@ struct WindowState { } impl WindowState { - fn render(&self, rect: Rect) -> bool { - self.context - .clear_rect(0.0, 0.0, self.get_width() as f64, self.get_height() as f64); + fn render(&self, invalid_rect: Rect) -> bool { let mut piet_ctx = piet_common::Piet::new(self.context.clone(), self.window.clone()); - let want_anim_frame = self.handler.borrow_mut().paint(&mut piet_ctx, rect); + let want_anim_frame = self.handler.borrow_mut().paint(&mut piet_ctx, invalid_rect); if let Err(e) = piet_ctx.finish() { log::error!("piet error on render: {:?}", e); } @@ -501,6 +499,7 @@ impl WindowHandle { if let Some(s) = self.0.upgrade() { let handle = self.clone(); let rect = s.invalid_rect.get(); + s.invalid_rect.set(Rect::ZERO); let state = s.clone(); s.request_animation_frame(move || { let want_anim_frame = state.render(rect); From dc9b5e97cd4afbf5caef19fa076f15da7dd6e833 Mon Sep 17 00:00:00 2001 From: Joe Neeman Date: Wed, 22 Apr 2020 15:22:39 -0500 Subject: [PATCH 7/7] Invalidate more conservatively when animating. --- druid/src/contexts.rs | 1 + druid/src/win_handler.rs | 7 ++++++- druid/src/window.rs | 5 ++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/druid/src/contexts.rs b/druid/src/contexts.rs index 78b0c59cd8..df89f149d5 100644 --- a/druid/src/contexts.rs +++ b/druid/src/contexts.rs @@ -474,6 +474,7 @@ impl<'a> LifeCycleCtx<'a> { /// Request an animation frame. pub fn request_anim_frame(&mut self) { self.base_state.request_anim = true; + self.request_paint(); } /// Submit a [`Command`] to be run after this event is handled. diff --git a/druid/src/win_handler.rs b/druid/src/win_handler.rs index bc2e5c2bb8..74372b1c9c 100644 --- a/druid/src/win_handler.rs +++ b/druid/src/win_handler.rs @@ -272,7 +272,12 @@ impl Inner { fn paint(&mut self, window_id: WindowId, piet: &mut Piet, rect: Rect) -> bool { if let Some(win) = self.windows.get_mut(window_id) { win.do_paint(piet, rect, &mut self.command_queue, &self.data, &self.env); - win.wants_animation_frame() + if win.wants_animation_frame() { + win.handle.invalidate(); + true + } else { + false + } } else { false } diff --git a/druid/src/window.rs b/druid/src/window.rs index a8ed68fc5b..6766e4c985 100644 --- a/druid/src/window.rs +++ b/druid/src/window.rs @@ -301,7 +301,10 @@ impl Window { self.layout(piet, queue, data, env); } - piet.clear(env.get(crate::theme::WINDOW_BACKGROUND_COLOR)); + piet.fill( + invalid_rect, + &env.get(crate::theme::WINDOW_BACKGROUND_COLOR), + ); self.paint(piet, invalid_rect, data, env); }