diff --git a/druid-shell/examples/invalidate.rs b/druid-shell/examples/invalidate.rs new file mode 100644 index 0000000000..2e4277083a --- /dev/null +++ b/druid-shell/examples/invalidate.rs @@ -0,0 +1,102 @@ +// 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::{Duration, Instant}; + +use druid_shell::kurbo::{Point, Rect}; +use druid_shell::piet::{Color, Piet, RenderContext}; + +use druid_shell::{Application, TimerToken, WinHandler, WindowBuilder, WindowHandle}; + +struct InvalidateTest { + handle: WindowHandle, + size: (f64, f64), + start_time: Instant, + color: Color, + rect: Rect, +} + +impl InvalidateTest { + fn update_color_and_rect(&mut self) { + let time_since_start = (Instant::now() - self.start_time).as_nanos(); + 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), + (_, _) => 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(); + self.handle.request_timer(Duration::from_millis(60)); + } + + fn timer(&mut self, _id: TimerToken) { + self.update_color_and_rect(); + 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 + } + + 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..9dcd5f30c1 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(), setNeedsDisplayInRect: 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..a84abdbced 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,13 @@ struct WindowState { window: web_sys::Window, canvas: web_sys::HtmlCanvasElement, context: web_sys::CanvasRenderingContext2d, + invalid_rect: Cell, } impl WindowState { - fn render(&self) -> 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); + 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); } @@ -370,6 +369,7 @@ impl WindowBuilder { window, canvas, context, + invalid_rect: Cell::new(Rect::ZERO), }); setup_web_callbacks(&window); @@ -411,7 +411,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 +498,11 @@ impl WindowHandle { fn render_soon(&self) { 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(); + 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..df89f149d5 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,13 +468,13 @@ 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. 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. @@ -475,14 +498,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 +528,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 +535,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 +738,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 +751,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..d3c775494e 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. @@ -327,7 +354,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. @@ -360,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)); }); } @@ -742,7 +769,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 +787,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 +819,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..cdd6bddca8 --- /dev/null +++ b/druid/src/widget/invalidation.rs @@ -0,0 +1,67 @@ +// 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); + 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; + } + + fn id(&self) -> Option { + self.inner.id() + } +} diff --git a/druid/src/widget/mod.rs b/druid/src/widget/mod.rs index ee0ed25a14..ce5c9aa7ec 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; diff --git a/druid/src/widget/scroll.rs b/druid/src/widget/scroll.rs index 44c5e6755e..36aede6070 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 { @@ -452,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/widget/widget_ext.rs b/druid/src/widget/widget_ext.rs index ce697c795e..41b432d799 100644 --- a/druid/src/widget/widget_ext.rs +++ b/druid/src/widget/widget_ext.rs @@ -14,6 +14,7 @@ //! Convenience methods for widgets. +use super::invalidation::DebugInvalidation; use super::{ Align, BackgroundBrush, Click, Container, Controller, ControllerHost, EnvScope, IdentityWrapper, Padding, Parse, SizedBox, WidgetId, @@ -181,6 +182,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..74372b1c9c 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,10 +269,15 @@ 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.wants_animation_frame() + win.do_paint(piet, rect, &mut self.command_queue, &self.data, &self.env); + if win.wants_animation_frame() { + win.handle.invalidate(); + true + } else { + false + } } else { false } @@ -436,8 +441,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 +616,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..6766e4c985 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, @@ -295,8 +301,11 @@ impl Window { self.layout(piet, queue, data, env); } - piet.clear(env.get(crate::theme::WINDOW_BACKGROUND_COLOR)); - self.paint(piet, data, env); + piet.fill( + invalid_rect, + &env.get(crate::theme::WINDOW_BACKGROUND_COLOR), + ); + self.paint(piet, invalid_rect, data, env); } fn layout(&mut self, piet: &mut Piet, queue: &mut CommandQueue, data: &T, env: &Env) { @@ -332,7 +341,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,16 +349,15 @@ 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)); + 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);