use { crate::makepad_draw::event::FingerLongPressEvent, crate::{ animator::*, makepad_derive_widget::*, makepad_draw::*, makepad_script::{script_err_wrong_value, ScriptFnRef}, scroll_bars::ScrollBars, widget::*, widget_async::{ CxWidgetToScriptCallExt, ScriptAsyncCalls, ScriptAsyncId, ScriptAsyncResult, }, widget_tree::CxWidgetExt, }, }; script_mod! { use mod.prelude.widgets_internal.* mod.widgets.ViewBase = set_type_default() do #(View::register_widget(vm)) } // maybe we should put an enum on the bools like #[derive(Script, ScriptHook, Clone, Copy)] pub enum ViewOptimize { #[pick] None, DrawList, Texture, } impl Default for ViewOptimize { fn default() -> Self { Self::None } } #[derive(Script, ScriptHook)] pub enum EventOrder { Down, #[pick] Up, #[live(Default::default())] List(Vec), } impl ViewOptimize { fn is_texture(&self) -> bool { if let Self::Texture = self { true } else { false } } fn is_draw_list(&self) -> bool { if let Self::DrawList = self { true } else { false } } fn needs_draw_list(&self) -> bool { return self.is_texture() || self.is_draw_list(); } } #[derive(Script, Animator, WidgetRef, WidgetSet, WidgetRegister)] pub struct View { #[uid] uid: WidgetUid, #[source] pub source: ScriptObjectRef, // draw info per UI element #[live] pub draw_bg: DrawQuad, #[live(false)] pub show_bg: bool, #[layout] pub layout: Layout, #[walk] pub walk: Walk, //#[live] use_cache: bool, #[live] dpi_factor: Option, #[live(false)] pub new_batch: bool, #[live(false)] pub texture_caching: bool, #[rust] optimize: ViewOptimize, #[live] event_order: EventOrder, #[live(true)] pub visible: bool, #[live(false)] skip_widget_tree_search: bool, #[live(true)] grab_key_focus: bool, #[live(false)] block_signal_event: bool, #[live] pub cursor: Option, #[live(false)] capture_overload: bool, #[live] scroll_bars: ScriptObjectRef, #[live(false)] design_mode: bool, #[live(false)] pub debug_stream: bool, #[live] on_render: ScriptFnRef, #[rust] script_async: ScriptAsyncCalls, #[rust] scroll_bars_obj: Option>, #[rust] view_size: Option, // Forces the next Texture-mode draw to re-render its offscreen pass instead of cache-hitting. // Set on a repopulate or optimize-mode flip, since neither moves the rect that will_redraw sees. #[rust] force_texture_redraw: bool, // Caps the offscreen texture's height (in Texture mode only) so a huge Fit-height view never // allocates a render target past the GPU's max texture size. Content past the cap is clipped. #[rust] texture_max_height: Option, #[rust] area: Area, #[rust] draw_list: Option, #[rust] texture_cache: Option, #[rust] defer_walks: SmallVec<[(LiveId, DeferredWalk); 1]>, #[rust] draw_state: DrawStateWrap, #[rust] pub children: SmallVec<[(LiveId, WidgetRef); 2]>, #[rust] live_update_order: SmallVec<[LiveId; 1]>, #[apply_default] animator: Animator, } struct ViewTextureCache { pass: DrawPass, _depth_texture: Texture, color_texture: Texture, } impl ScriptHook for View { fn on_before_apply( &mut self, _vm: &mut ScriptVm, apply: &Apply, _scope: &mut Scope, _value: ScriptValue, ) { if apply.is_reload() { self.live_update_order.clear(); } } fn on_after_apply( &mut self, vm: &mut ScriptVm, apply: &Apply, scope: &mut Scope, value: ScriptValue, ) { // Handle children from the object's vec // Skip for eval applies - eval should only affect this widget's own properties, // not propagate to children (which would use inherited vec from prototype) if !apply.is_eval() { if let Some(obj) = value.as_object() { let mut anon_index = 0usize; vm.vec_with(obj, |vm, vec| { for kv in vec { // Determine the id: use prefixed id if available, otherwise use numbered id for anonymous children let id = if let Some(id) = kv.key.as_id() { Some(id) } else if kv.key.is_nil() { // Anonymous child widget - use numbered id let id = LiveId(anon_index as u64); anon_index += 1; Some(id) } else { None }; if let Some(id) = id { if !WidgetRef::value_is_newable_widget(vm, kv.value) { continue; } if apply.is_reload() { self.live_update_order.push(id); } if let Some((_, node)) = self.children.iter_mut().find(|(id2, _)| *id2 == id) { node.script_apply(vm, apply, scope, kv.value); } else { let widget = WidgetRef::script_from_value_scoped(vm, scope, kv.value); self.children.push((id, widget)); } } } }); } } if apply.is_reload() { // update/delete children list // Only reorder and truncate if we actually processed items from the vec. // If vec was empty but children exist, this is likely an incomplete parse // during streaming - preserve existing children. if !self.live_update_order.is_empty() || self.children.is_empty() { for (idx, id) in self.live_update_order.iter().enumerate() { if let Some(pos) = self.children.iter().position(|(i, _v)| *i == *id) { self.children.swap(idx, pos); } } self.children.truncate(self.live_update_order.len()); } } if self.texture_caching { self.optimize = ViewOptimize::Texture; } else if self.new_batch { self.optimize = ViewOptimize::DrawList; } if self.optimize.needs_draw_list() && self.draw_list.is_none() { self.draw_list = Some(DrawList2d::script_new(vm)); } if !self.scroll_bars.is_zero() { if self.scroll_bars_obj.is_none() { self.scroll_bars_obj = Some(Box::new(ScrollBars::script_from_value( vm, self.scroll_bars.as_object().into(), ))); } } vm.cx_mut().widget_tree_mark_dirty(self.uid); } } #[derive(Clone, Debug, Default)] pub enum ViewAction { #[default] None, FingerDown(FingerDownEvent), FingerUp(FingerUpEvent), FingerLongPress(FingerLongPressEvent), FingerMove(FingerMoveEvent), FingerHoverIn(FingerHoverEvent), FingerHoverOut(FingerHoverEvent), KeyDown(KeyEvent), KeyUp(KeyEvent), } impl View { /// Build the object `on_render` appends its children to. It is re-applied to this /// View with `Apply::Reload`, so whatever isn't reachable on its prototype chain /// reverts to the base widget's defaults. /// /// It therefore protos off the *instance* source — the declaration site — not the /// instance's template. Protoing off `source.proto` skipped the declaration level /// and silently dropped every property set there: `results := ScrollYView{height: 610}` /// re-applied as the base `height: Fill`, which a `Fit` parent defers to zero, so /// the view (and every rendered row in it) vanished after the first `render()`. /// /// `script_result` restores `self.source` afterwards, so the chain stays /// `me -> declaration` instead of growing a link per render. `no_vec` leaves `me`'s /// own vec empty so `on_render` fully owns the child list. fn make_render_me(&self, vm: &mut ScriptVm) -> ScriptValue { if self.source.is_zero() { return NIL; } vm.bx .heap .new_with_proto_no_vec(self.source.as_object().into()) .into() } pub fn set_debug_dump(&mut self, cx: &mut Cx, debug: bool) { if let Some(draw_list) = &self.draw_list { cx.draw_lists[draw_list.id()].debug_dump = debug; } } pub fn repaint(&mut self, cx: &mut Cx) { if let Some(draw_list) = &self.draw_list { if let Some(pass_id) = cx.draw_lists[draw_list.id()].draw_pass_id { cx.repaint_pass(pass_id); } } } } impl ViewRef { pub fn set_debug_dump(&self, cx: &mut Cx, debug: bool) { if let Some(mut inner) = self.borrow_mut() { inner.set_debug_dump(cx, debug); } } pub fn repaint(&self, cx: &mut Cx) { if let Some(mut inner) = self.borrow_mut() { inner.repaint(cx); } } /// Forces the next Texture-mode draw to re-render its offscreen texture. Call after /// repopulating a texture-cached view whose content changed. pub fn redraw_texture_cache(&self) { if let Some(mut inner) = self.borrow_mut() { inner.redraw_texture_cache(); } } /// Caps the offscreen texture's height in Texture mode (`None` = uncapped). See the `View` /// method for details. pub fn set_texture_max_height(&self, max: Option) { if let Some(mut inner) = self.borrow_mut() { inner.set_texture_max_height(max); } } pub fn finger_down(&self, actions: &Actions) -> Option { if let Some(item) = actions.find_widget_action(self.widget_uid()) { if let ViewAction::FingerDown(fd) = item.cast() { return Some(fd); } } None } pub fn finger_up(&self, actions: &Actions) -> Option { if let Some(item) = actions.find_widget_action(self.widget_uid()) { if let ViewAction::FingerUp(fd) = item.cast() { return Some(fd); } } None } pub fn finger_move(&self, actions: &Actions) -> Option { if let Some(item) = actions.find_widget_action(self.widget_uid()) { if let ViewAction::FingerMove(fd) = item.cast() { return Some(fd); } } None } pub fn finger_hover_in(&self, actions: &Actions) -> Option { if let Some(item) = actions.find_widget_action(self.widget_uid()) { if let ViewAction::FingerHoverIn(fd) = item.cast() { return Some(fd); } } None } pub fn finger_hover_out(&self, actions: &Actions) -> Option { if let Some(item) = actions.find_widget_action(self.widget_uid()) { if let ViewAction::FingerHoverOut(fd) = item.cast() { return Some(fd); } } None } pub fn key_down(&self, actions: &Actions) -> Option { if let Some(item) = actions.find_widget_action(self.widget_uid()) { if let ViewAction::KeyDown(fd) = item.cast() { return Some(fd); } } None } pub fn key_up(&self, actions: &Actions) -> Option { if let Some(item) = actions.find_widget_action(self.widget_uid()) { if let ViewAction::KeyUp(fd) = item.cast() { return Some(fd); } } None } pub fn animator_cut(&self, cx: &mut Cx, state: &[LiveId; 2]) { if let Some(mut inner) = self.borrow_mut() { inner.animator_cut(cx, state); } } pub fn animator_play(&self, cx: &mut Cx, state: &[LiveId; 2]) { if let Some(mut inner) = self.borrow_mut() { inner.animator_play(cx, state); } } pub fn animator_play_with(&self, cx: &mut Cx, state: &[LiveId; 2], play: Play) { if let Some(mut inner) = self.borrow_mut() { inner.animator_play_with(cx, state, play); } } pub fn toggle_state( &self, cx: &mut Cx, is_state_1: bool, animate: Animate, state1: &[LiveId; 2], state2: &[LiveId; 2], ) { if let Some(mut inner) = self.borrow_mut() { inner.animator_toggle(cx, is_state_1, animate, state1, state2); } } pub fn set_visible(&self, cx: &mut Cx, visible: bool) { if let Some(mut inner) = self.borrow_mut() { inner.set_visible(cx, visible) } } pub fn visible(&self) -> bool { if let Some(inner) = self.borrow() { inner.visible() } else { false } } pub fn set_texture(&self, slot: usize, texture: &Texture) { if let Some(mut inner) = self.borrow_mut() { inner.draw_bg.set_texture(slot, texture); } } pub fn set_uniform(&self, cx: &Cx, uniform: LiveId, value: &[f32]) { if let Some(mut inner) = self.borrow_mut() { inner.draw_bg.set_uniform(cx, uniform, value); } } pub fn set_scroll_pos(&self, cx: &mut Cx, v: Vec2d) { if let Some(mut inner) = self.borrow_mut() { inner.set_scroll_pos(cx, v) } } pub fn area(&self) -> Area { if let Some(inner) = self.borrow_mut() { inner.area } else { Area::Empty } } pub fn cached_texture_id(&self) -> Option { self.borrow().and_then(|inner| { inner .texture_cache .as_ref() .map(|cache| cache.color_texture.texture_id()) }) } pub fn child_count(&self) -> usize { if let Some(inner) = self.borrow_mut() { inner.children.len() } else { 0 } } pub fn set_key_focus(&self, cx: &mut Cx) { if let Some(inner) = self.borrow_mut() { inner.set_key_focus(cx); } } } impl ViewSet { pub fn animator_cut(&self, cx: &mut Cx, state: &[LiveId; 2]) { for item in self.iter() { item.animator_cut(cx, state) } } pub fn animator_play(&self, cx: &mut Cx, state: &[LiveId; 2]) { for item in self.iter() { item.animator_play(cx, state); } } pub fn animator_play_with(&self, cx: &mut Cx, state: &[LiveId; 2], play: Play) { for item in self.iter() { item.animator_play_with(cx, state, play); } } pub fn toggle_state( &self, cx: &mut Cx, is_state_1: bool, animate: Animate, state1: &[LiveId; 2], state2: &[LiveId; 2], ) { for item in self.iter() { item.toggle_state(cx, is_state_1, animate, state1, state2); } } pub fn set_visible(&self, cx: &mut Cx, visible: bool) { for item in self.iter() { item.set_visible(cx, visible) } } pub fn set_texture(&self, slot: usize, texture: &Texture) { for item in self.iter() { item.set_texture(slot, texture) } } pub fn set_uniform(&self, cx: &Cx, uniform: LiveId, value: &[f32]) { for item in self.iter() { item.set_uniform(cx, uniform, value) } } pub fn redraw(&self, cx: &mut Cx) { for item in self.iter() { item.redraw(cx); } } pub fn finger_down(&self, actions: &Actions) -> Option { for item in self.iter() { if let Some(e) = item.finger_down(actions) { return Some(e); } } None } pub fn finger_up(&self, actions: &Actions) -> Option { for item in self.iter() { if let Some(e) = item.finger_up(actions) { return Some(e); } } None } pub fn finger_move(&self, actions: &Actions) -> Option { for item in self.iter() { if let Some(e) = item.finger_move(actions) { return Some(e); } } None } pub fn key_down(&self, actions: &Actions) -> Option { for item in self.iter() { if let Some(e) = item.key_down(actions) { return Some(e); } } None } pub fn key_up(&self, actions: &Actions) -> Option { for item in self.iter() { if let Some(e) = item.key_up(actions) { return Some(e); } } None } } impl WidgetNode for View { fn widget_uid(&self) -> WidgetUid { self.uid } fn walk(&mut self, _cx: &mut Cx) -> Walk { self.walk } fn area(&self) -> Area { self.area } fn set_scroll_pos(&mut self, cx: &mut Cx, v: Vec2d) { self.set_scroll_pos(cx, v); } fn redraw(&mut self, cx: &mut Cx) { self.area.redraw(cx); if let Some(draw_list) = &self.draw_list { draw_list.redraw(cx); } for (_, child) in &mut self.children { child.redraw(cx); } } fn children(&self, visit: &mut dyn FnMut(LiveId, WidgetRef)) { for (id, child) in &self.children { visit(*id, child.clone()); } } fn skip_widget_tree_search(&self) -> bool { self.skip_widget_tree_search } fn find_widgets_from_point(&self, cx: &Cx, point: DVec2, found: &mut dyn FnMut(&WidgetRef)) { for (_, child) in &self.children { child.find_widgets_from_point(cx, point, found); } } fn selection_text_len(&self) -> usize { for (_, child) in &self.children { let v = child.selection_text_len(); if v > 0 { return v; } } 0 } fn selection_point_to_char_index(&self, cx: &Cx, abs: DVec2) -> Option { for (_, child) in &self.children { if let Some(v) = child.selection_point_to_char_index(cx, abs) { return Some(v); } } None } fn selection_set(&mut self, anchor: usize, cursor: usize) { for (_, child) in &self.children { child.selection_set(anchor, cursor); } } fn selection_clear(&mut self) { for (_, child) in &self.children { child.selection_clear(); } } fn selection_select_all(&mut self) { for (_, child) in &self.children { child.selection_select_all(); } } fn selection_get_text_for_range(&self, start: usize, end: usize) -> String { for (_, child) in &self.children { let v = child.selection_get_text_for_range(start, end); if !v.is_empty() { return v; } } String::new() } fn selection_get_full_text(&self) -> String { for (_, child) in &self.children { let v = child.selection_get_full_text(); if !v.is_empty() { return v; } } String::new() } fn set_visible(&mut self, cx: &mut Cx, visible: bool) { if self.visible != visible { self.visible = visible; // A view that has never drawn has an empty area, so its own // redraw is a no-op and the visibility change only lands on // the next unrelated full repaint (the classic "appears after // hot reload" bug). Escalate to a full redraw in that case. if visible && matches!(self.area, Area::Empty) { cx.redraw_all(); } else { self.redraw(cx); } } } fn visible(&self) -> bool { self.visible } } impl Widget for View { fn script_call( &mut self, vm: &mut ScriptVm, method: LiveId, args: ScriptValue, ) -> ScriptAsyncResult { if method == live_id!(render) { let me = self.make_render_me(vm); return vm.with_cx_mut(|cx| { cx.widget_to_script_async_call_fwd( self.uid, &mut self.script_async, me, self.source.clone(), self.on_render.clone(), args, id!(render), ) }); } if method == live_id!(set_visible) { if let Some(args_obj) = args.as_object() { let trap = vm.bx.threads.cur().trap.pass(); let value = vm.bx.heap.vec_value(args_obj, 0, trap); let visible = value .as_bool() .or_else(|| value.as_number().map(|n| n != 0.0)); match visible { Some(visible) => { vm.with_cx_mut(|cx| { if self.visible != visible { self.visible = visible; self.redraw(cx); } }); } // Never guess on a bad argument: a silent default (especially // `true`) turns script bugs into invisible misbehavior. Keep // the current visibility and surface an error instead. None => { return ScriptAsyncResult::Return(script_err_wrong_value!( vm.trap(), "set_visible expects a bool (or number), got {:?}", value.value_type() )); } } } return ScriptAsyncResult::Return(NIL); } ScriptAsyncResult::MethodNotFound } fn script_result(&mut self, vm: &mut ScriptVm, id: ScriptAsyncId, result: ScriptValue) { let Some(call) = self.script_async.take(id) else { return; }; if call.method() == id!(render) { if result.is_err() { // An error mid-closure abandons every child emitted before it // and used to do so with ZERO diagnostics — the "on_render // silently renders nothing" family cost days to bisect. Say // what happened. let msg = vm.bx.heap.temp_string_with(|heap, out| { heap.cast_to_string(result, out); out.to_string() }); error!("on_render closure failed; discarding its output: {msg}"); return; } if let Some(me_obj) = call.me().as_object() { // The closure's FINAL statement becomes its return value (the // parser converts the last commit into a RETURN), so a widget // emitted last would otherwise vanish. Commit it as the last // child; non-widget values are skipped downstream anyway. if let Some(ret_obj) = result.as_object() { if ret_obj != me_obj { // Unchecked: `me` is this render's own throwaway // object (make_render_me), never frozen. vm.bx.heap.vec_push_unchecked(me_obj, NIL, result); } } // `#[source]` is reassigned by any non-eval script apply, so this Reload // would leave `source` pointing at `me`. Keep it on the declaration object: // the next `make_render_me` protos off it (see there), and `me` is a // throwaway whose children already hold their own refs. let declaration = self.source.clone(); self.script_apply(vm, &Apply::Reload, &mut Scope::empty(), me_obj.into()); self.source = declaration; self.redraw(vm.cx_mut()); } } } fn is_interactive(&self) -> bool { self.cursor.is_some() || self.animator.is_defined } fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { if !self.visible && event.requires_visibility() { return; } let uid = self.widget_uid(); if self.animator_handle_event(cx, event).must_redraw() { self.redraw(cx); } if self.block_signal_event { if let Event::Signal = event { return; } } if let Event::ClearHover = event { if self.animator.is_defined { self.animator_play(cx, ids!(hover.off)); self.animator_play(cx, ids!(down.off)); } } // A press that catches an in-progress momentum fling stops the scroll and is consumed: // it must not also activate a child widget under the finger, as on iOS, Android, and // macOS. So when the scroll bars report a caught fling, skip dispatching this press to // children. This runs before the child loop below, since children would otherwise // capture the press first. let mut fling_caught = false; if let Some(scroll_bars) = &mut self.scroll_bars_obj { let mut actions = Vec::new(); scroll_bars.handle_main_event(cx, event, scope, &mut actions); if actions.len() > 0 { cx.redraw_area_and_children(self.area); }; fling_caught = scroll_bars.catch_fling_on_press(cx, event); } if !fling_caught { match &self.event_order { EventOrder::Up => { for (_id, child) in self.children.iter_mut().rev() { child.handle_event(cx, event, scope); } } EventOrder::Down => { for (_id, child) in self.children.iter_mut() { child.handle_event(cx, event, scope); } } EventOrder::List(list) => { for id in list { if let Some((_, child)) = self.children.iter_mut().find(|(id2, _)| id2 == id) { child.handle_event(cx, event, scope); } } } } } event.hit_tweak_ray(self.area(), self.widget_uid()); // Also skip this View's own press-driven hit handling when a fling was caught, so the // catching press is fully consumed (it neither activates a child nor fires this // View's own FingerDown/animator). `fling_caught` is only ever true on a press event // over a flinging scroll view, so key/hover handling is unaffected. if !fling_caught && (self.visible && self.cursor.is_some() || self.animator.is_defined) { match event.hits_with_capture_overload(cx, self.area(), self.capture_overload) { Hit::FingerDown(e) => { if self.grab_key_focus { cx.set_key_focus(self.area()); } cx.widget_action(uid, ViewAction::FingerDown(e)); if self.animator.is_defined { self.animator_play(cx, ids!(down.on)); } } Hit::FingerMove(e) => cx.widget_action(uid, ViewAction::FingerMove(e)), Hit::FingerLongPress(e) => cx.widget_action(uid, ViewAction::FingerLongPress(e)), Hit::FingerUp(e) => { cx.widget_action(uid, ViewAction::FingerUp(e)); if self.animator.is_defined { self.animator_play(cx, ids!(down.off)); } } Hit::FingerHoverIn(e) => { cx.widget_action(uid, ViewAction::FingerHoverIn(e)); if let Some(cursor) = &self.cursor { cx.set_cursor(*cursor); } if self.animator.is_defined { self.animator_play(cx, ids!(hover.on)); } } Hit::FingerHoverOut(e) => { cx.widget_action(uid, ViewAction::FingerHoverOut(e)); if self.animator.is_defined { self.animator_play(cx, ids!(hover.off)); } } Hit::KeyDown(e) => cx.widget_action(uid, ViewAction::KeyDown(e)), Hit::KeyUp(e) => cx.widget_action(uid, ViewAction::KeyUp(e)), _ => (), } } if let Some(scroll_bars) = &mut self.scroll_bars_obj { scroll_bars.handle_scroll_event(cx, event, scope, &mut Vec::new()); } } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { // the beginning state if self.draw_state.begin(cx, DrawState::Drawing(0, false)) { if !self.visible { self.draw_state.end(); return DrawStep::done(); } self.defer_walks.clear(); match self.optimize { ViewOptimize::Texture => { let walk = self.walk_from_previous_size(walk); // A repopulate or optimize-mode flip forces one real re-render: the size-based // cache check can't see content changes on a recycled/toggled view. let force = std::mem::take(&mut self.force_texture_redraw); // Re-render whenever the on-screen rect changes (position or size) or a // repopulate/mode-flip forced it; only a truly unchanged view cache-hits. if !force && !cx.will_redraw(self.draw_list.as_mut().unwrap(), walk) { if let Some(texture_cache) = &self.texture_cache { self.draw_bg .draw_vars .set_texture(0, &texture_cache.color_texture); let mut rect = cx.walk_turtle_with_area(&mut self.area, walk); // NOTE(eddyb) see comment lower below for why this is // disabled (it used to match `set_pass_scaled_area`). if false { rect.size *= 2.0 / self.dpi_factor.unwrap_or(1.0); } self.draw_bg.draw_abs(cx, rect); self.area = self.draw_bg.area(); /*if false { // FIXME(eddyb) this was the previous logic, // but the only tested apps that use `CachedView` // are sized correctly (regardless of `dpi_factor`) // *without* extra scaling here. cx.set_pass_scaled_area( &texture_cache.pass, self.area, 2.0 / self.dpi_factor.unwrap_or(1.0), ); } else {*/ cx.set_pass_area(&texture_cache.pass, self.area); //} } return DrawStep::done(); } // lets start a pass if self.texture_cache.is_none() { self.texture_cache = Some(ViewTextureCache { pass: DrawPass::new(cx), _depth_texture: Texture::new(cx), color_texture: Texture::new(cx), }); let texture_cache = self.texture_cache.as_mut().unwrap(); //cache.pass.set_depth_texture(cx, &cache.depth_texture, PassClearDepth::ClearWith(1.0)); texture_cache.color_texture = Texture::new_with_format( cx, TextureFormat::RenderBGRAu8 { size: TextureSize::Auto, initial: true, }, ); texture_cache.pass.set_color_texture( cx, &texture_cache.color_texture, DrawPassClearColor::ClearWith(vec4(0.0, 0.0, 0.0, 0.0)), ); } let texture_cache = self.texture_cache.as_mut().unwrap(); cx.make_child_pass(&texture_cache.pass); cx.begin_pass(&texture_cache.pass, self.dpi_factor); self.draw_list.as_mut().unwrap().begin_always(cx) } ViewOptimize::DrawList => { let walk = self.walk_from_previous_size(walk); if self .draw_list .as_mut() .unwrap() .begin(cx, walk) .is_not_redrawing() { cx.walk_turtle_with_area(&mut self.area, walk); return DrawStep::done(); } } _ => (), } // ok so.. we have to keep calling draw till we return LiveId(0) let scroll = if let Some(scroll_bars) = &mut self.scroll_bars_obj { scroll_bars.begin_nav_area(cx); scroll_bars.get_scroll_pos() } else { self.layout.scroll }; // Only Texture mode allocates an offscreen render target sized to this turtle, so cap // its height there. None/DrawList render into the parent turtle and must stay uncapped. let walk = if self.optimize.is_texture() { self.walk_with_texture_max_height(walk) } else { walk }; if self.show_bg { /*if let Some(image_texture) = &self.image_texture { self.draw_bg.draw_vars.set_texture(0, image_texture); }*/ self.draw_bg .begin(cx, walk, self.layout.with_scroll(scroll)); //.with_scale(2.0 / self.dpi_factor.unwrap_or(2.0))); } else { cx.begin_turtle(walk, self.layout.with_scroll(scroll)); //.with_scale(2.0 / self.dpi_factor.unwrap_or(2.0))); } } while let Some(DrawState::Drawing(step, resume)) = self.draw_state.get() { if step < self.children.len() { //let id = self.draw_order[step]; if let Some((id, child)) = self.children.get_mut(step) { if child.visible() { let walk = child.walk(cx); if resume { child.draw_walk(cx, scope, walk)?; } else if let Some(fw) = cx.defer_walk_turtle(walk) { self.defer_walks.push((*id, fw)); } else { self.draw_state.set(DrawState::Drawing(step, true)); child.draw_walk(cx, scope, walk)?; } } } self.draw_state.set(DrawState::Drawing(step + 1, false)); } else { self.draw_state.set(DrawState::DeferWalk(0)); } } while let Some(DrawState::DeferWalk(step)) = self.draw_state.get() { if step < self.defer_walks.len() { let (id, dw) = &mut self.defer_walks[step]; if let Some((_id, child)) = self.children.iter_mut().find(|(id2, _)| id2 == id) { let walk = dw.resolve(cx); child.draw_walk(cx, scope, walk)?; } self.draw_state.set(DrawState::DeferWalk(step + 1)); } else { if let Some(scroll_bars) = &mut self.scroll_bars_obj { scroll_bars.draw_scroll_bars(cx); }; if self.show_bg { if self.optimize.is_texture() { panic!("dont use show_bg and texture caching at the same time"); } self.draw_bg.end(cx); self.area = self.draw_bg.area(); } else { cx.end_turtle_with_area(&mut self.area); }; if let Some(scroll_bars) = &mut self.scroll_bars_obj { scroll_bars.set_area(self.area); scroll_bars.end_nav_area(cx); }; // Track measured size in every mode. The None path must update it too, or a later // Texture draw sizes its offscreen turtle to a stale height and clips the content. self.view_size = Some(self.area.rect(cx).size); if self.optimize.needs_draw_list() { let rect = self.area.rect(cx); self.draw_list.as_mut().unwrap().end(cx); if self.optimize.is_texture() { let texture_cache = self.texture_cache.as_mut().unwrap(); cx.end_pass(&texture_cache.pass); /*if cache.pass.id_equals(4){ self.draw_bg.draw_vars.set_uniform(cx, id!(marked),&[1.0]); } else{ self.draw_bg.draw_vars.set_uniform(cx, id!(marked),&[0.0]); }*/ self.draw_bg .draw_vars .set_texture(0, &texture_cache.color_texture); self.draw_bg.draw_abs(cx, rect); let area = self.draw_bg.area(); let texture_cache = self.texture_cache.as_mut().unwrap(); /* if false { // FIXME(eddyb) this was the previous logic, // but the only tested apps that use `CachedView` // are sized correctly (regardless of `dpi_factor`) // *without* extra scaling here. cx.set_pass_scaled_area( &texture_cache.pass, area, 2.0 / self.dpi_factor.unwrap_or(1.0), ); } else {*/ cx.set_pass_area(&texture_cache.pass, area); //} } } self.draw_state.end(); } } DrawStep::done() } } trait EventTweakRayExt { fn hit_tweak_ray(&self, area: Area, widget_uid: WidgetUid); } impl EventTweakRayExt for Event { fn hit_tweak_ray(&self, area: Area, widget_uid: WidgetUid) { if let Event::TweakRay(e) = self { if !area.is_empty() { e.hit_widget_uids.borrow_mut().push(widget_uid.0); } } } } #[derive(Clone)] enum DrawState { Drawing(usize, bool), DeferWalk(usize), } impl View { pub fn swap_child(&mut self, pos_a: usize, pos_b: usize) { self.children.swap(pos_a, pos_b); } pub fn child_index(&mut self, comp: &WidgetRef) -> Option { if let Some(pos) = self.children.iter().position(|(_, w)| w == comp) { Some(pos) } else { None } } pub fn child_at_index(&mut self, index: usize) -> Option<&WidgetRef> { if let Some(f) = self.children.get(index) { Some(&f.1) } else { None } } pub fn set_scroll_pos(&mut self, cx: &mut Cx, v: Vec2d) { if let Some(scroll_bars) = &mut self.scroll_bars_obj { scroll_bars.set_scroll_pos(cx, v); } else { self.layout.scroll = v; } } pub fn area(&self) -> Area { self.area } /// Switch this view's draw optimization at runtime (e.g. toggle texture caching per frame). /// Allocates the backing draw_list on demand when promoting to a draw-list-backed mode. pub fn set_optimize(&mut self, cx: &mut Cx, optimize: ViewOptimize) { // A mode flip (e.g. None<->Texture) is a guaranteed content/layout change, and the None // path never updates view_size, so force one fresh texture re-render on the change. if core::mem::discriminant(&self.optimize) != core::mem::discriminant(&optimize) { self.redraw_texture_cache(); } self.optimize = optimize; if self.optimize.needs_draw_list() && self.draw_list.is_none() { self.draw_list = Some(DrawList2d::new(cx)); } } /// Forces the next Texture-mode draw to re-render its offscreen texture instead of compositing /// the cached one, and drops the stale measured size so the re-render sizes to the new content. /// Call after repopulating a texture-cached view's children. pub fn redraw_texture_cache(&mut self) { self.force_texture_redraw = true; self.view_size = None; } /// Caps the offscreen texture's height when this view is in Texture mode. `None` (the default) /// leaves it uncapped. Only useful for a Fit-height cached view whose content can be taller than /// the GPU's max texture size; content past the cap is clipped. pub fn set_texture_max_height(&mut self, max: Option) { if self.texture_max_height != max { self.texture_max_height = max; self.redraw_texture_cache(); } } fn walk_with_texture_max_height(&self, walk: Walk) -> Walk { match self.texture_max_height { Some(max) if !walk.height.is_fill() => Walk { height: Size::Fixed(max), ..walk }, _ => walk, } } pub fn walk_from_previous_size(&self, walk: Walk) -> Walk { // Fill and Fixed sizes are already known before drawing, so keep them live — // a Fixed size can be fresh truth for this frame (e.g. a deferred fill the // parent just resolved), and pinning it to the previous frame's measurement // would make the cached-draw dirty check miss a pure size change. Only // content-driven sizes fall back to the previous measurement, since they // cannot be known before the children draw. let view_size = self.view_size.unwrap_or(Vec2d::default()); Walk { abs_pos: walk.abs_pos, width: if walk.width.is_fill() || walk.width.is_fixed() { walk.width } else { Size::Fixed(view_size.x) }, height: if walk.height.is_fill() || walk.height.is_fixed() { walk.height } else { Size::Fixed(view_size.y) }, margin: walk.margin, metrics: Metrics::default(), } } pub fn child_count(&self) -> usize { self.children.len() } pub fn debug_print_children(&self) { // Debug output removed } pub fn debug_props(&self) -> String { format!( "walk=({:?},{:?}) flow={:?} show_bg={} visible={} padding=({},{},{},{}) spacing={:?}", self.walk.width, self.walk.height, self.layout.flow, self.show_bg, self.visible, self.layout.padding.left, self.layout.padding.top, self.layout.padding.right, self.layout.padding.bottom, self.layout.spacing ) } pub fn debug_dump_tree(&self, depth: usize) -> String { let mut out = String::new(); let indent = " ".repeat(depth); out.push_str(&format!("{}View {{ {} }}\n", indent, self.debug_props())); for (id, child) in &self.children { out.push_str(&format!("{} [id={:?}] ", indent, id)); if let Some(view) = child.borrow_mut::() { out.push_str(&format!("\n{}", view.debug_dump_tree(depth + 2))); } else { let text = child.text(); if text.is_empty() { out.push_str("\n"); } else { out.push_str(&format!("\n", text)); } } } out } pub fn set_key_focus(&self, cx: &mut Cx) { cx.set_key_focus(self.draw_bg.area()); } }