diff --git a/Cargo.lock b/Cargo.lock index d19f865..74f7d1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -511,6 +511,7 @@ dependencies = [ "euclid", "femtovg", "flate2", + "fns", "futures", "geo", "geo-macros", @@ -1187,6 +1188,15 @@ dependencies = [ "spin", ] +[[package]] +name = "fns" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d318f82a68feac152dab48e8a6f1eb665a915ea6a0c76e4ad5ed137f80c368" +dependencies = [ + "log", +] + [[package]] name = "fnv" version = "1.0.7" diff --git a/Cargo.toml b/Cargo.toml index 8662f69..0593771 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,6 +94,7 @@ slippy-map-tiles = "0.16.0" reqwest = "0.11.25" url = "2.5.0" quick_cache = "0.4.1" +fns = "0.0.7" [build-dependencies] diff --git a/src/components/monitor/monitor.rs b/src/components/monitor/monitor.rs index 07a5f9c..6d3110d 100644 --- a/src/components/monitor/monitor.rs +++ b/src/components/monitor/monitor.rs @@ -26,10 +26,12 @@ use crate::map_tile_utils::lat_lon_to_zoom; use crate::pipeline::element::Target; use adw::prelude::*; use femtovg::ImageId; +use fns::debounce; use relm4::{component::Component, *}; use slippy_map_tiles::Tile; use tokio::task; use tracing::instrument::WithSubscriber; +use crate::utils::estimate_zoom_level; #[derive(Debug)] pub enum MonitorCommand { @@ -42,8 +44,12 @@ pub struct MonitorModel { render_range: (f64, f64, f64, f64), sidebar_open: bool, sidebar_width: i32, + #[do_not_track] + debouncer: Rc, zoom: u8, #[do_not_track] + last_call: Rc>, + #[do_not_track] map_tile_getter: Rc, new_layer: i8, #[no_eq] @@ -99,17 +105,17 @@ impl Component for MonitorModel { connect_render_status_notify[sender] => move |r| { sender.output(MonitorOutputMsg::LayerRenderFinished); }, - connect_range_changing_notify[sender] => move |r| { - sender.input(MonitorInputMsg::RefreshTiles); - }, - connect_scale_notify[sender] => move |r| { - let scale_factor = r.scale(); - let zoom = scale_factor.log2().round() as u8; - let new_zoom = model.zoom + zoom; - if model.zoom != new_zoom { - sender.input(MonitorInputMsg::ChangeZoom(new_zoom)); - } - }, + // connect_range_changing_notify[sender] => move |r| { + // sender.input(MonitorInputMsg::RefreshTiles); + // }, + // connect_scale_notify => move |r| { + // let ((x1,x2), (y1,y2)) = r.render_range(); + // let (w, h) = r.window_size(); + // let new_zoom = estimate_zoom_level(x1,x2,y1,y2,w as f64, h as f64); + // if model.zoom != new_zoom { + // debouncer(new_zoom); + // } + // }, set_interior_layers: model.layers.clone(), }, add_overlay=>k::Button{ @@ -152,6 +158,8 @@ impl Component for MonitorModel { } MonitorInputMsg::SetRenderRange(lon_start, lon_end, lat_start, lat_end) => { self.set_render_range((lat_start, lat_end, lon_start, lon_end)); + let r = &widgets.renderer; + (*self.debouncer)(10); } MonitorInputMsg::ClearMetaItems => self.sidebar.emit(SideBarInputMsg::ClearMetaItems), MonitorInputMsg::UpdateMetaItem(map) => { @@ -206,6 +214,16 @@ impl Component for MonitorModel { padding: [20.0, 40.0, 20.0, 40.0], }; + let new_sender = sender.clone(); + + let _debouncer = fns::debounce(move |new_zoom: u8| { + new_sender.input(MonitorInputMsg::ChangeZoom(new_zoom)); + }, std::time::Duration::from_millis(500)); + + let debouncer = move |zoom: u8| { + _debouncer.call(zoom); + }; + let mut model = MonitorModel { render_range: (4.0, 53.3, 73.3, 135.0), new_layer: 0, @@ -213,13 +231,18 @@ impl Component for MonitorModel { render_cfg, sidebar_open: true, sidebar_width: 400, + last_call : Rc::new(RefCell::new(std::time::Instant::now())), layers: init, zoom: 4, + debouncer : Rc::new(debouncer), map_tile_getter: Rc::new(MapTile::default()), sidebar, tracker: 0, }; + let last_call = model.last_call.clone(); + let debouncer = model.debouncer.clone(); + let widgets = view_output! {}; ComponentParts { model, widgets } } diff --git a/src/map_tile.rs b/src/map_tile.rs index a6049c8..50b02f5 100644 --- a/src/map_tile.rs +++ b/src/map_tile.rs @@ -34,7 +34,7 @@ impl Default for MapTile { Self { server: "https://tiles.stadiamaps.com/tiles/".to_string(), api_key: "06f1aeed-5d91-48e3-9ce5-1e99063f7f73".to_string(), - style: "stamen_terrain/".to_string(), + style: "stamen_toner".to_string(), client: Client::new(), onloading: Arc::new(std::sync::Mutex::new( HashSet::new())), cache: Arc::new(std::sync::Mutex::new(Cache::new(32))), @@ -58,7 +58,7 @@ impl MapTile { fn get_tile_task(&self, tile: &Tile) -> BoxFuture<'static, Result, Error>> { let base_url = Url::parse(&self.server).unwrap(); let mut request_url = base_url - .join(&self.style) + .join(&format!("{}/", self.style)) .unwrap() .join(&format!("{}/{}/{}.png", tile.zoom(), tile.x(), tile.y())) .unwrap(); @@ -164,7 +164,6 @@ impl MapTile { if let Some(target) = cache.get(&tile) { results.push(target); } else { - debug!("Tile {:?} is not in cache", tile); let center = tile.center_point(); let mut start_zoom = zoom - 1; while start_zoom > 0 { diff --git a/src/utils.rs b/src/utils.rs index c993281..73a6ec7 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -12,6 +12,10 @@ use surfman::{ }; use crate::widgets::render::predefined::color_mapper::BoundaryNorm; +use std::sync::Arc; +use std::time::{Duration}; +use tokio::{time, sync::{Mutex, mpsc, oneshot}}; +use crate::RUNTIME; pub fn meshgrid(x: ArrayView1, y: ArrayView1) -> (Array2, Array2) where @@ -331,3 +335,25 @@ pub fn create_kdp_boundarynorm() -> BoundaryNorm { -125.0, ) } + + +pub fn estimate_zoom_level(lat_min: f64, lon_min: f64, lat_max: f64, lon_max: f64, screen_width: f64, screen_height: f64) -> u8 { + let r: f64 = 6371.0; + let avg_lat = (lat_min + lat_max) / 2.0; + // 将经纬度范围转换为在该纬度下的公里数 + let delta_lon = (lon_max - lon_min) * (r * std::f64::consts::PI / 180.0) * (avg_lat * std::f64::consts::PI / 180.0).cos().abs(); + let delta_lat = (lat_max - lat_min) * 111.32; + + // 估算每个像素代表的实际距离(公里/像素) + let km_per_pixel_x = delta_lon / screen_width; + let km_per_pixel_y = delta_lat / screen_height; + + // 选择较小的比例尺 + let km_per_pixel = km_per_pixel_x.min(km_per_pixel_y); + + // 根据比例尺估算Zoom Level + // 这里的比例尺和Zoom Level的对应关系可能需要根据实际地图服务进行调整 + let zoom_level_estimation = 14.0 - km_per_pixel.log10(); + + zoom_level_estimation.round() as u8 +} \ No newline at end of file diff --git a/src/widgets/render/imp.rs b/src/widgets/render/imp.rs index 412e0a6..5db5265 100644 --- a/src/widgets/render/imp.rs +++ b/src/widgets/render/imp.rs @@ -3,19 +3,19 @@ use super::interior::InteriorWidget; use super::{Layer, WindowCoord}; use crate::coords::proj::Mercator; use crate::coords::Mapper; +use crate::map_tile::MapTile; use crate::pipeline::element::{Target, TargetType}; use femtovg::{Canvas, Color, FontId, Paint, Renderer}; use gtk::glib::{self, prelude::*, Properties}; use gtk::subclass::prelude::*; use gtk::traits::{GLAreaExt, WidgetExt}; +use slippy_map_tiles::Tile; use std::cell::{Cell, RefCell}; use std::collections::HashMap; use std::num::NonZeroU32; use std::rc::Rc; use std::sync::{Arc, Mutex}; -use slippy_map_tiles::Tile; use tracing::info; -use crate::map_tile::MapTile; #[derive(Debug, Default, Clone, Copy, PartialEq)] pub struct RenderConfig { @@ -41,12 +41,11 @@ pub struct RenderStatus { pub(super) scale_rate: Option, pub(super) pointer_location: WindowCoord, pub(super) motion: RenderMotion, - pub(super) scale: f32, - pub(super) translate: Option<(f32, f32)>, - pub(super) view_range: Option<((f64, f64), (f64, f64))>, - translation: Option<(f64, f64)>, - init_scale_rate: f64, - init_translation: (f64, f64), + pub(super) view_range: ((f64, f64), (f64, f64)), + pub(super) translation: (f64, f64), + pub(super) init_scale_rate: f64, + pub(super) init_lat_lon: ((f64, f64), (f64, f64)), + pub(super) init_translation: (f64, f64), } #[derive(Properties)] @@ -119,7 +118,10 @@ impl WidgetImpl for Render {} impl GLAreaImpl for Render { fn resize(&self, width: i32, height: i32) { - self.status.borrow_mut().window_size = Some((width, height)); + { + let mut status = self.status.borrow_mut(); + status.window_size = Some((width, height)); + } self.ensure_canvas(); let mut canvas = self.canvas.borrow_mut(); let canvas = canvas.as_mut().unwrap(); @@ -129,33 +131,35 @@ impl GLAreaImpl for Render { self.obj().scale_factor() as f32, ); - let translation = self.status.borrow().translation; - let scale_rate = self.status.borrow().scale_rate; - let mapper = self.mapper.borrow(); + let scale_rate = self.status.borrow_mut().scale_rate; - if let None = translation { + if let Some(scale) = scale_rate { + let ((x1,x2), (y1, y2)) = self.view_range().unwrap(); + // let (x1, y1) = self.map((x1, y1)).unwrap(); + // let (x2, y2) = self.map((x2, y2)).unwrap(); + // let scale = ((y2 - y1) / height as f32).min((x2 - x1) / width as f32) as f64; + // let mut status = self.status.borrow_mut(); + // status.scale_rate = Some(scale); + } else { let mut status = self.status.borrow_mut(); - status.translation = Some((mapper.get_bounds().0, mapper.get_bounds().2)); - status.init_translation = (mapper.get_bounds().0, mapper.get_bounds().2); - } - - if let None = scale_rate { - let scale = (mapper.get_bounds().3 - mapper.get_bounds().2) / canvas.height() as f64; - self.status.borrow_mut().scale_rate = Some(scale); + let ((y1, y2), (x1, x2)) = status.init_lat_lon; + let mapper = self.mapper.borrow(); + let (x1, y1) = mapper.map((x1, y1)).unwrap(); + let (x2, y2) = mapper.map((x2, y2)).unwrap(); + let scale = ((y2 - y1) / height as f64).min((x2 - x1) / width as f64); + status.scale_rate = Some(scale); } } fn render(&self, context: >k::gdk::GLContext) -> bool { self.ensure_canvas(); - let configs = self.config.borrow(); - let (w, h) = { + + { let mut canvas = self.canvas.borrow_mut(); let canvas = canvas.as_mut().unwrap(); - let dpi = self.obj().scale_factor(); let w = canvas.width(); let h = canvas.height(); - canvas.clear_rect( 0, 0, @@ -163,78 +167,27 @@ impl GLAreaImpl for Render { (h as i32 * dpi) as u32, Color::rgba(0, 0, 0, 255), ); - (w, h) }; let binding = self.tiles.borrow(); if let Some(tiles) = binding.as_ref() { - let ((x1 , x2), (y1,y2)) = self.view_range().unwrap(); - let mut tiles = tiles.current_tiles( ((y1 as f32, y2 as f32), (x1 as f32, x2 as f32))); + let ((x1, x2), (y1, y2)) = self.view_range().unwrap(); + let mut tiles = tiles.current_tiles(((y1 as f32, y2 as f32), (x1 as f32, x2 as f32))); for tile in tiles.iter_mut() { let mut binding = (*tile).lock().unwrap(); self.draw_target(&mut *binding); } } - let render_range = self.status.borrow().view_range.clone(); - if let Some(((lat1, lat2), (lon1, lon2))) = render_range { - let mut status = self.status.borrow_mut(); - - let mapper = self.mapper.borrow(); - - let (tx, ty) = mapper.map((lon1, lat1)).unwrap(); - status.translation.replace((tx, ty)); - status.init_translation = (tx, ty); - - let (lon1, lat1) = mapper.map((lon1, lat1)).unwrap(); - let (lon2, lat2) = mapper.map((lon2, lat2)).unwrap(); - - let scale = ((lat1 - lat2).abs() / h as f64).max((lon1 - lon2).abs() / w as f64); - status.scale_rate.replace(scale); - status.init_scale_rate = scale; - - status.view_range = None; - } else { - let mut status = self.status.borrow_mut(); - match status.motion { - RenderMotion::Translate => { - if let Some((x, y)) = status.translate { - let (ix, iy) = status.init_translation; - status.translation = Some(( - ix + x as f64 * status.scale_rate.unwrap(), - iy + y as f64 * status.scale_rate.unwrap(), - )); - } else { - status.init_translation = status.translation.unwrap(); - } - } - RenderMotion::Scale => { - let scale_rate = status.scale_rate.unwrap(); - let scale_flag = status.scale as f64; - let initial = status.init_scale_rate; - let step = scale_rate * 0.1; - - let (tx, ty) = status.translation.unwrap(); - let (px, py) = status.pointer_location; - - let scaled = scale_rate + scale_flag * step; - status.scale_rate = Some(scaled); - self.obj().set_scale(initial / scaled); - let sx = scale_flag * step * px as f64; - let sy = scale_flag * step * py as f64; - status.translation = Some((tx - sx, ty - sy)); - status.init_translation = status.translation.unwrap(); - } - RenderMotion::None => {} - } + { + let configs = self.config.borrow(); + let c = self.interior_layers.borrow(); + let c = &mut *c.borrow_mut(); + self.interior + .borrow() + .draw(c, &self.obj(), self.status.borrow(), configs); } - let c = self.interior_layers.borrow(); - let c = &mut *c.borrow_mut(); - self.interior - .borrow() - .draw(c, &self.obj(), self.status.borrow(), configs); - { let mut canvas = self.canvas.borrow_mut(); let canvas = canvas.as_mut().unwrap(); @@ -293,33 +246,27 @@ impl Render { h as f32 - padding[0] - padding[2], ); let (w, h) = (w as f64, h as f64); - let mapper = self.mapper.borrow(); - let status = self.status.borrow(); - status.translation.and_then(|(tx, ty)| { - status.scale_rate.and_then(|scale| { - let (x1, y1) = (tx + padding[3] as f64, ty + padding[2] as f64); - let (x2, y2) = ( - tx + w * scale + padding[3] as f64, - ty + h * scale + padding[2] as f64, - ); - let (x1, y1) = mapper.inverse_map((x1, y1)).unwrap(); - let (x2, y2) = mapper.inverse_map((x2, y2)).unwrap(); - Some(((x1, x2), (y1, y2))) - }) - }) + let (tx, ty) = status.translation; + let scale = status.scale_rate; + let (x1, y1) = (tx + padding[3] as f64, ty + padding[2] as f64); + let (x2, y2) = ( + tx + w * scale.unwrap() + padding[3] as f64, + ty + h * scale.unwrap() + padding[2] as f64, + ); + let (x1, y1) = mapper.inverse_map((x1, y1)).unwrap(); + let (x2, y2) = mapper.inverse_map((x2, y2)).unwrap(); + Some(((x1, x2), (y1, y2))) } pub(super) fn inverse_map(&self, loc: (f32, f32)) -> Option<(f64, f64)> { let (x, y) = loc; let status = self.status.borrow(); - status.translation.and_then(|(tx, ty)| { - status.scale_rate.and_then(|scale| { - let (x, y) = (x as f64, y as f64); - Some((tx + x * scale, (ty + y * scale))) - }) - }) + let (tx, ty) = status.translation; + let scale = status.scale_rate; + let (x, y) = (x as f64, y as f64); + Some((tx + x * scale.unwrap(), (ty + y * scale.unwrap()))) } fn padding(&self) -> [f32; 4] { @@ -330,19 +277,13 @@ impl Render { let (x, y) = loc; let (_, h) = self.window_size().unwrap(); let status = self.status.borrow(); - status.translation.and_then(|(tx, ty)| { - status.scale_rate.and_then(|scale| { - Some(( - (x - tx as f64) as f32 / scale as f32, - h as f32 - (y - ty as f64) as f32 / scale as f32, - )) - }) - }) - } + let (tx, ty) = status.translation; + let scale = status.scale_rate; - pub(super) fn set_translation(&self, translation: (f64, f64)) { - let mut status = self.status.borrow_mut(); - status.translation = Some(translation); + Some(( + (x - tx) as f32 / scale.unwrap() as f32, + h as f32 - (y - ty) as f32 / scale.unwrap() as f32, + )) } fn pointer_loc(&self) -> (f32, f32) { @@ -353,10 +294,21 @@ impl Render { pub(super) fn set_view(&self, range: (f64, f64, f64, f64)) { let (lat1, lat2, lon1, lon2) = range; - self.status - .borrow_mut() - .view_range - .replace(((lat1, lat2), (lon1, lon2))); + let mapper = self.mapper.borrow(); + let lb = mapper.map((lon1, lat1)).unwrap(); + let rt = mapper.map((lon2, lat2)).unwrap(); + let mut s = self.status.borrow_mut(); + s.translation = (lb.0, lb.1); + s.init_translation = (lb.0, lb.1); + s.init_lat_lon = ((lat1, lat2), (lon1, lon2)); + + if let Some((w, h)) = s.window_size { + let scale = ((rt.0 - lb.0) / w as f64).min((rt.1 - lb.1) / h as f64); + s.scale_rate = Some(scale); + } else { + s.scale_rate = None; + } + s.view_range = ((lat1, lat2), (lon1, lon2)); } pub(super) fn draw_target(&self, target: &mut Target) { @@ -365,13 +317,15 @@ impl Render { let obj = self.obj(); - let (ox,oy) = target.origin(&obj); + let (ox, oy) = target.origin(&obj); let (x, y) = target.size(&obj); - let id = match target.target{ - TargetType:: ImageId(id) => id, + let id = match target.target { + TargetType::ImageId(id) => id, TargetType::Mem(ref mem) => { - let converted = canvas.load_image_mem(mem, femtovg::ImageFlags::empty()).unwrap(); + let converted = canvas + .load_image_mem(mem, femtovg::ImageFlags::empty()) + .unwrap(); target.set_target(TargetType::ImageId(converted)); converted } diff --git a/src/widgets/render/mod.rs b/src/widgets/render/mod.rs index 9102250..5f7679d 100644 --- a/src/widgets/render/mod.rs +++ b/src/widgets/render/mod.rs @@ -6,25 +6,26 @@ pub mod predefined; pub mod renders; pub mod widget; // pub use self::cms::CMS; -use crate::coords::cms::CMS; pub use self::imp::{RenderConfig, RenderMotion, RenderStatus}; +use crate::components::messages::MonitorInputMsg; +use crate::coords::cms::CMS; use crate::coords::{Mapper, Range}; +use crate::errors::PipelineError; +use crate::map_tile::MapTile; +use crate::pipeline::element::{Target, TargetType}; use adw::prelude::{GLAreaExt, GestureDragExt}; +use femtovg::ImageFlags; use geo_types::LineString; use glib::clone; pub use glib::subclass::prelude::*; use gtk::traits::WidgetExt; use gtk::{EventControllerScrollFlags, Inhibit}; pub use interior::*; +use slippy_map_tiles::Tile; use std::cell::{Ref, RefCell, RefMut}; use std::collections::HashMap; use std::rc::Rc; use std::sync::{Arc, Mutex}; -use femtovg::ImageFlags; -use slippy_map_tiles::Tile; -use crate::errors::PipelineError; -use crate::map_tile::MapTile; -use crate::pipeline::element::{Target, TargetType}; pub type WindowCoord = (f32, f32); @@ -42,15 +43,12 @@ impl Default for Render { impl Render { pub fn new(mapper: Option, cfg: RenderConfig) -> Self { let this: Self = glib::Object::new(); - { - let mut status = this.imp().status.borrow_mut(); - status.scale = 1.0; - } this.imp().config.replace(cfg); if let Some(mapper) = mapper { this.set_mapper(mapper); } + let dpi = this.scale_factor() as f32; let pointer_location_detecture = gtk::EventControllerMotion::new(); pointer_location_detecture.connect_motion( clone!( @weak this as r => move |_context, x, y| { @@ -64,35 +62,57 @@ impl Render { ); let scale_detecture = gtk::EventControllerScroll::new(EventControllerScrollFlags::VERTICAL); + let _r = Mutex::new(this.clone()); + + let debouncer = fns::debounce( + move |scale: f64| { + _r.lock().unwrap().set_scale(scale); + }, + std::time::Duration::from_millis(500), + ); + scale_detecture.connect_scroll(clone!( - @weak this as r => @default-panic,move |_context, _x, y| { - r.update_status(|status|{ - status.scale = y as f32; - status.motion = RenderMotion::Scale; - }); - r.queue_render(); - Inhibit(false) - } + @weak this as r => @default-panic,move |_context, _x, y| { + let mut rate = 0.0; + r.update_status(|s|{ + let scale_rate = s.scale_rate.unwrap(); + let scale_flag = y as f64; + let step = scale_rate * 0.1; + let (tx, ty) = s.translation; + let (px, py) = s.pointer_location; + let scaled = scale_rate + scale_flag * step; + s.scale_rate = Some(scaled); + rate = scaled / 1.0; + let sx = scale_flag * step * px as f64; + let sy = scale_flag * step * py as f64; + s.translation = (tx - sx, ty - sy); + s.init_translation = s.translation; + s.motion = RenderMotion::Scale; + }); + r.queue_render(); + debouncer.call(rate); + Inhibit(true) + } )); let drag_detecture = gtk::GestureDrag::new(); - drag_detecture.connect_drag_update(clone!( - @weak this as r => move |this, _, _| { + drag_detecture.connect_drag_update(clone!(@weak this as r => move |this, _, _| { let (ox, oy) = this.offset().unwrap_or((0.0,0.0)); - let dpi = r.scale_factor() as f32; r.update_status(|s| { - s.translate = Some((-ox as f32 * dpi , oy as f32 * dpi)); + let (ix, iy) = s.init_translation; + let x = -ox as f32 * dpi; + let y = oy as f32 * dpi; + s.translation = (ix + x as f64 * s.scale_rate.unwrap(), iy + y as f64 * s.scale_rate.unwrap()); s.motion = RenderMotion::Translate; }); - r.set_range_changing(0.0); + r.set_range_changing(0.0); r.queue_render(); })); drag_detecture.connect_drag_end(clone!( @weak this as r => move |_,_,_|{ - r.update_status(|cfg| { - cfg.translate = None; - cfg.motion = RenderMotion::Translate; + r.update_status(|s| { + s.init_translation = s.translation; }) } r.queue_render(); @@ -103,7 +123,6 @@ impl Render { this.add_controller(pointer_location_detecture); this.add_controller(scale_detecture); this.add_controller(drag_detecture); - this } @@ -221,12 +240,25 @@ impl Render { self.queue_render(); } - pub fn load_img_mem(&self, img: &[u8], origin:(f64,f64),size :(f32,f32), bounds: (Range, Range)) -> Result { + pub fn load_img_mem( + &self, + img: &[u8], + origin: (f64, f64), + size: (f32, f32), + bounds: (Range, Range), + ) -> Result { let mut canvas = self.get_canvas(); let cvs = canvas.as_mut().unwrap(); let img_id = cvs.load_image_mem(img, ImageFlags::empty()).unwrap(); let (width, height) = size; - Ok(Target::new(TargetType::ImageId(img_id), width, height, bounds, None, None)) + Ok(Target::new( + TargetType::ImageId(img_id), + width, + height, + bounds, + None, + None, + )) } pub fn delete_img(&self, img: Target) { @@ -236,8 +268,6 @@ impl Render { cvs.delete_image(id); } } - - pub fn get_scale(&self) -> f32 { - self.imp().status.borrow().scale - } } + +unsafe impl Send for Render {} \ No newline at end of file