use super::*; pub(crate) fn build_window_attributes() -> WindowAttributes { let (w, h) = A::initial_size(); let attrs = WindowAttributes::default() .with_title(A::title()) .with_inner_size(LogicalSize::new(w, h)); // En Linux, `with_name` del trait de Wayland mapea al `app_id` del // xdg-toplevel — lo que el compositor (`mirada-compositor`) usa para // reconocer ventanas especiales (greeter, launcher…). #[cfg(all(target_os = "linux", not(target_os = "android")))] { if let Some(id) = A::app_id() { use llimphi_hal::winit::platform::wayland::WindowAttributesExtWayland; return attrs.with_name(id, ""); } } attrs } impl ApplicationHandler> for Runtime { fn resumed(&mut self, event_loop: &ActiveEventLoop) { if self.state.is_some() { return; } let window = event_loop .create_window(build_window_attributes::()) .expect("create window"); let window = Arc::new(window); // IME opt-in: sólo se habilita si la app lo pide (ver `App::ime_allowed`). // Con IME activo el texto compuesto llega por `WindowEvent::Ime`. if A::ime_allowed() { window.set_ime_allowed(true); } let hal = pollster::block_on(Hal::new(None)).expect("hal"); let surface = WinitSurface::new(&hal, window.clone()).expect("surface"); let renderer = Renderer::new(&hal).expect("renderer"); let overlay_compositor = llimphi_hal::OverlayCompositor::new(&hal.device); let typesetter = llimphi_text::Typesetter::new(); window.request_redraw(); self.state = Some(RuntimeState { window, hal, surface, renderer, scene: vello::Scene::new(), overlay_compositor, model: Some(A::init(&self.handle)), cursor: PhysicalPosition::new(0.0, 0.0), modifiers: Modifiers::default(), typesetter, layout: LayoutTree::new(), overlay_layout: LayoutTree::new(), last_render: None, hovered: None, drag: None, focused: None, last_title: None, }); // Sincroniza el factor de escala inicial (el de la ventana recién // creada) ANTES del primer render: así una app que dependa del DPI // (p. ej. `devicePixelRatio` en puriy) ya lo tiene correcto en su // primera pasada, sin esperar a un ScaleFactorChanged. if let Some(state) = self.state.as_mut() { let scale = state.window.scale_factor(); if let Some(msg) = A::on_scale_factor(state.model.as_ref().expect("model"), scale) { let model = state.model.take().expect("model"); state.model = Some(A::update(model, msg, &self.handle)); state.last_render = None; } } } fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) { match event { UserEvent::Quit => event_loop.exit(), UserEvent::Msg(msg) => { // Un Msg del canal (Handle::dispatch, ticks periódicos, trabajo // de fondo) muta el modelo compartido y repinta TODAS las // ventanas — así un cambio se refleja tanto en la primaria como // en las secundarias (config) sin importar de dónde vino. self.dispatch_model(msg); } UserEvent::OpenWindow { key, title, width, height } => { self.open_secondary(event_loop, key, title, width, height); } UserEvent::CloseWindow { key } => { if let Some(pos) = self.secondaries.iter().position(|s| s.key == key) { // Drop de la SecondaryState → se destruye la ventana/surface. self.secondaries.remove(pos); } } } } fn window_event( &mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent, ) { // ¿El evento es de una ventana secundaria? Lo atiende su handler // dedicado (path aparte: la primaria queda 100% intacta). if let Some(idx) = self.secondaries.iter().position(|s| s.window.id() == _id) { self.handle_secondary_event(idx, event); return; } let Some(state) = self.state.as_mut() else { return; }; match event { WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::Resized(size) => { state.surface.resize(size.width, size.height); // La app puede reaccionar al nuevo viewport (emitir un // evento `resize`, recalcular layout, etc.). El update se // corre tras reconfigurar la surface; el cache se invalida // para repintar con el tamaño nuevo. if let Some(msg) = A::on_resize(state.model.as_ref().expect("model"), size.width, size.height) { let model = state.model.take().expect("model"); state.model = Some(A::update(model, msg, &self.handle)); state.last_render = None; } state.window.request_redraw(); } WindowEvent::ScaleFactorChanged { scale_factor, .. } => { // El DPI de la ventana cambió (movida a otro monitor, escalado // del sistema). winit envía un Resized aparte para el nuevo // tamaño físico; aquí sólo propagamos el factor. if let Some(msg) = A::on_scale_factor(state.model.as_ref().expect("model"), scale_factor) { let model = state.model.take().expect("model"); state.model = Some(A::update(model, msg, &self.handle)); state.last_render = None; } state.window.request_redraw(); } WindowEvent::CursorMoved { position, .. } => { let prev_cursor = state.cursor; state.cursor = position; // Drag activo: dispatchear delta al handler + actualizar // tracking del drop target hovereado (solo si hay payload). if let Some(drag) = state.drag.as_mut() { let dx = (position.x - drag.last_cursor.x) as f32; let dy = (position.y - drag.last_cursor.y) as f32; drag.last_cursor = position; let payload_active = drag.payload.is_some(); let mut need_redraw = false; if dx != 0.0 || dy != 0.0 { let msg_opt = match &drag.handler { DragHandlerKind::Delta(h) => h(DragPhase::Move, dx, dy), DragHandlerKind::DeltaAt(h, lx0, ly0) => { h(DragPhase::Move, dx, dy, *lx0, *ly0) } }; if let Some(msg) = msg_opt { let model = state.model.take().expect("model"); state.model = Some(A::update(model, msg, &self.handle)); // Durante drag NO invalidamos el cache — // queda válido para el próximo Move. need_redraw = true; } } if payload_active { if let Some(cache) = state.last_render.as_mut() { let new_drop = hit_test_drop( &cache.mounted, &cache.computed, position.x as f32, position.y as f32, ); if new_drop != cache.drop_hover_idx { cache.drop_hover_idx = new_drop; need_redraw = true; } } } if need_redraw { state.window.request_redraw(); } } else { // Sin drag: chequear hover. Si hay overlay, el // hover-test va contra él; el árbol principal queda // congelado mientras el overlay esté arriba. // // Además del repintado (para el `hover_fill`), si el // nodo recién hovereado declara un `on_pointer_enter`, // lo dispatcheamos: es lo que permite, p.ej., cambiar // de menú con el mouse o abrir un submenú al pasar por // encima. Extraemos el Msg en un scope para soltar el // borrow del cache antes de mutar el modelo. let mut enter_msg: Option = None; let mut hovered_changed = false; let mut new_hovered: Option = state.hovered; if let Some(cache) = state.last_render.as_ref() { let (mounted, computed) = match cache.overlay.as_ref() { Some(ov) => (&ov.mounted, &ov.computed), None => (&cache.mounted, &cache.computed), }; let new_hover = hit_test_hover( mounted, computed, position.x as f32, position.y as f32, ); // Comparamos contra el hover PERSISTENTE (state.hovered), // no contra el del cache: el render recomputa el del cache // al cursor actual cada cuadro, así que en una app que // re-renderiza sin parar la transición de hover se perdería // (y el hover-switch de menús no andaría). Ver `hovered`. if new_hover != state.hovered { hovered_changed = true; enter_msg = new_hover .and_then(|i| mounted.nodes.get(i)) .and_then(|n| n.on_pointer_enter.clone()); } new_hovered = new_hover; } state.hovered = new_hovered; if hovered_changed { state.window.request_redraw(); } if let Some(msg) = enter_msg { let model = state.model.take().expect("model"); state.model = Some(A::update(model, msg, &self.handle)); // El estado cambió → invalidamos el cache para // re-render (p.ej. el submenú que se abre). state.last_render = None; } let _ = prev_cursor; } } WindowEvent::ModifiersChanged(mods) => { state.modifiers = mods.state().into(); } WindowEvent::Ime(ime) if A::ime_allowed() => { use llimphi_hal::winit::event::Ime; let ev = match ime { Ime::Enabled => ImeEvent::Enabled, Ime::Preedit(text, cursor) => ImeEvent::Preedit { text, cursor }, Ime::Commit(text) => ImeEvent::Commit(text), Ime::Disabled => ImeEvent::Disabled, }; if let Some(msg) = A::on_ime(state.model.as_ref().expect("model"), &ev) { let model = state.model.take().expect("model"); state.model = Some(A::update(model, msg, &self.handle)); state.last_render = None; state.window.request_redraw(); } } WindowEvent::KeyboardInput { event, .. } => { // Tab / Shift+Tab mueven el foco entre nodos `focusable`, // que administra el runtime. Sólo intercepta si hay // enfocables y en Pressed; si no, cae al `on_key` normal // (apps que usan Tab para otra cosa lo siguen recibiendo). let is_tab = event.state == ElementState::Pressed && matches!(event.logical_key, Key::Named(NamedKey::Tab)); if is_tab { let order = state .last_render .as_ref() .map(|c| focus_order(&c.mounted, &c.computed)) .unwrap_or_default(); if !order.is_empty() { let next = next_focus(&order, state.focused, state.modifiers.shift); state.focused = next; if let Some(msg) = A::on_focus(state.model.as_ref().expect("model"), next) { let model = state.model.take().expect("model"); state.model = Some(A::update(model, msg, &self.handle)); } state.last_render = None; state.window.request_redraw(); return; } } let ev = KeyEvent { key: event.logical_key.clone(), state: match event.state { ElementState::Pressed => KeyState::Pressed, ElementState::Released => KeyState::Released, }, text: event.text.as_ref().map(|t| t.to_string()), modifiers: state.modifiers, repeat: event.repeat, }; if let Some(msg) = A::on_key(state.model.as_ref().expect("model"), &ev) { let model = state.model.take().expect("model"); state.model = Some(A::update(model, msg, &self.handle)); state.last_render = None; state.window.request_redraw(); } } WindowEvent::DroppedFile(path) => { // Un evento por archivo (winit los entrega serializados); si // el usuario suelta varios, el bucle re-entra y aplicamos // updates en orden. if let Some(msg) = A::on_file_drop(state.model.as_ref().expect("model"), path) { let model = state.model.take().expect("model"); state.model = Some(A::update(model, msg, &self.handle)); state.last_render = None; state.window.request_redraw(); } } WindowEvent::MouseWheel { delta, .. } => { // Convención winit: LineDelta es líneas; PixelDelta es // píxeles físicos (touchpads). En CSS y aquí, positivo // (rueda hacia adelante / dos dedos arriba) = scroll // hacia arriba, así que invertimos `y` para que el // contenido "siga al dedo" en y positivo. `x` queda // como llega. let wd = match delta { MouseScrollDelta::LineDelta(x, y) => WheelDelta { x, y: -y }, MouseScrollDelta::PixelDelta(p) => WheelDelta { x: (p.x as f32) / 20.0, y: -(p.y as f32) / 20.0, }, }; let cursor = (state.cursor.x as f32, state.cursor.y as f32); // Primero: ¿hay un nodo con `on_scroll` bajo el cursor? Si // consume el evento (`Some`), no cae al `on_wheel` global. // El overlay tiene prioridad, igual que con clicks. Se // extrae el handler en un scope para soltar el borrow del // cache antes de mutar el modelo. let scroll_handler: Option> = if let Some(cache) = state.last_render.as_ref() { let (m, c) = match cache.overlay.as_ref() { Some(ov) => (&ov.mounted, &ov.computed), None => (&cache.mounted, &cache.computed), }; hit_test_scroll(m, c, cursor.0, cursor.1) .and_then(|i| m.nodes[i].on_scroll.clone()) } else { None }; let msg = match scroll_handler { Some(h) => h(wd.x, wd.y), None => A::on_wheel( state.model.as_ref().expect("model"), wd, cursor, state.modifiers, ), }; if let Some(msg) = msg { let model = state.model.take().expect("model"); state.model = Some(A::update(model, msg, &self.handle)); state.last_render = None; state.window.request_redraw(); } } WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left, .. } => { // Hit-test contra el cache del último redraw (siempre // representa lo visible). Fallback raro: cache vacío. let cursor = state.cursor; // Click-to-focus: si el click cae sobre un nodo enfocable, // el runtime le da el foco ANTES de procesar la acción de // click. Extraemos el id en un scope (suelta el borrow del // cache) y recién después mutamos el foco/modelo. let focus_hit = state .last_render .as_ref() .and_then(|cache| { let (m, c) = match cache.overlay.as_ref() { Some(ov) => (&ov.mounted, &ov.computed), None => (&cache.mounted, &cache.computed), }; hit_test_focusable(m, c, cursor.x as f32, cursor.y as f32) }); if focus_hit.is_some() && focus_hit != state.focused { state.focused = focus_hit; if let Some(msg) = A::on_focus(state.model.as_ref().expect("model"), focus_hit) { let model = state.model.take().expect("model"); state.model = Some(A::update(model, msg, &self.handle)); } state.last_render = None; } // Tupla: (drag_fn, drag_at_fn, payload, on_click_msg, // on_click_at_handler, rect: (x, y, w, h)) type HitInfo = ( Option>, Option>, Option, Option, Option>, Option<(f32, f32, f32, f32)>, ); let lookup_hit = |m: &Mounted, c: &ComputedLayout| -> Option> { hit_test_click(m, c, cursor.x as f32, cursor.y as f32).map(|i| { let node = &m.nodes[i]; let rect = c.get(node.id).map(|r| (r.x, r.y, r.w, r.h)); ( node.drag.clone(), node.drag_at.clone(), node.drag_payload, node.on_click.clone(), node.on_click_at.clone(), rect, ) }) }; // Con overlay activo, los clicks van EXCLUSIVAMENTE a él. // Si el cursor cae sobre un nodo del overlay sin handler, // el click se descarta — la convención de "scrim que // dismissa" pide que la app meta su propio fondo // clicable con `on_click = DismissOverlay`. let idx_and_action: Option> = if let Some(cache) = state.last_render.as_ref() { if let Some(ov) = cache.overlay.as_ref() { lookup_hit(&ov.mounted, &ov.computed) } else { lookup_hit(&cache.mounted, &cache.computed) } } else { let model_ref = state.model.as_ref().expect("model"); let view = A::view(model_ref); let overlay_view = A::view_overlay(model_ref); let mut layout = LayoutTree::new(); let mounted: Mounted = mount(&mut layout, view); let (w, h) = state.surface.size(); let ts = &mut state.typesetter; let computed = { let tmap = &mounted.text_measures; layout .compute_with_measure(mounted.root, (w as f32, h as f32), |nid, known, avail| { match tmap.get(&nid) { Some(tm) => measure_text_node(ts, tm, known, avail), None => llimphi_layout::taffy::Size::ZERO, } }) .expect("layout") }; if let Some(ov) = overlay_view { let mut olay = LayoutTree::new(); let omounted: Mounted = mount(&mut olay, ov); let ocomp = { let tmap = &omounted.text_measures; olay .compute_with_measure(omounted.root, (w as f32, h as f32), |nid, known, avail| { match tmap.get(&nid) { Some(tm) => measure_text_node(ts, tm, known, avail), None => llimphi_layout::taffy::Size::ZERO, } }) .expect("layout overlay") }; lookup_hit(&omounted, &ocomp) } else { lookup_hit(&mounted, &computed) } }; // drag_at + on_click_at COEXISTEN: el press dispara // on_click_at (si está) y arranca un drag rastreado con la // posición inicial. Diseño pensado para canvas elements // que necesitan select-on-press + move-on-drag. // // En cambio, `drag` simple (sin _at) mantiene la semántica // antigua: gana exclusivo sobre on_click. if let Some((_, Some(handler_at), payload, _, click_at, Some((ox, oy, rw, rh)))) = &idx_and_action { let lx0 = cursor.x as f32 - ox; let ly0 = cursor.y as f32 - oy; // Disparar on_click_at en el press (si también está). if let Some(click_at_h) = click_at { if let Some(msg) = click_at_h(lx0, ly0, *rw, *rh) { let model = state.model.take().expect("model"); state.model = Some(A::update(model, msg, &self.handle)); state.last_render = None; } } state.drag = Some(DragState { handler: DragHandlerKind::DeltaAt(handler_at.clone(), lx0, ly0), last_cursor: cursor, payload: *payload, }); state.window.request_redraw(); } else if let Some((Some(handler), _, payload, _, _, _)) = &idx_and_action { state.drag = Some(DragState { handler: DragHandlerKind::Delta(handler.clone()), last_cursor: cursor, payload: *payload, }); // Si hay payload, repintar para que el drop target // bajo cursor (si lo hay) se ilumine de entrada. if payload.is_some() { if let Some(cache) = state.last_render.as_mut() { let new_drop = hit_test_drop( &cache.mounted, &cache.computed, cursor.x as f32, cursor.y as f32, ); if new_drop != cache.drop_hover_idx { cache.drop_hover_idx = new_drop; state.window.request_redraw(); } } } } else if let Some((_, _, _, _, Some(handler), Some((ox, oy, rw, rh)))) = &idx_and_action { // on_click_at gana sobre on_click si ambos existen. let lx = cursor.x as f32 - ox; let ly = cursor.y as f32 - oy; if let Some(msg) = handler(lx, ly, *rw, *rh) { let model = state.model.take().expect("model"); state.model = Some(A::update(model, msg, &self.handle)); state.last_render = None; state.window.request_redraw(); } } else if let Some((_, _, _, Some(msg), _, _)) = idx_and_action { let model = state.model.take().expect("model"); state.model = Some(A::update(model, msg, &self.handle)); state.last_render = None; state.window.request_redraw(); } } WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Middle, .. } => { // Middle-click: dispatcha `on_middle_click` del nodo // bajo cursor si lo declaró. La capa overlay tiene // prioridad (mismo razonamiento que el left/right click). let cursor = state.cursor; let lookup = |m: &Mounted, c: &ComputedLayout| -> Option { hit_test_middle_click(m, c, cursor.x as f32, cursor.y as f32) .and_then(|i| m.nodes[i].on_middle_click.clone()) }; let msg = if let Some(cache) = state.last_render.as_ref() { if let Some(ov) = cache.overlay.as_ref() { lookup(&ov.mounted, &ov.computed) } else { lookup(&cache.mounted, &cache.computed) } } else { None }; if let Some(msg) = msg { let model = state.model.take().expect("model"); state.model = Some(A::update(model, msg, &self.handle)); state.last_render = None; state.window.request_redraw(); } } WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Right, .. } => { // Right-click: dispatcheamos `on_right_click` o // `on_right_click_at` del nodo bajo cursor. La capa // overlay tiene prioridad (mismo razonamiento que el // left-click). Nodos sin handler de right-click no // reaccionan — no "filtramos" al left. let cursor = state.cursor; let lookup = |m: &Mounted, c: &ComputedLayout| -> Option<(Option, Option>, (f32, f32, f32, f32))> { hit_test_right_click(m, c, cursor.x as f32, cursor.y as f32).map(|i| { let node = &m.nodes[i]; let rect = c .get(node.id) .map(|r| (r.x, r.y, r.w, r.h)) .unwrap_or((0.0, 0.0, 0.0, 0.0)); ( node.on_right_click.clone(), node.on_right_click_at.clone(), rect, ) }) }; let hit = if let Some(cache) = state.last_render.as_ref() { if let Some(ov) = cache.overlay.as_ref() { lookup(&ov.mounted, &ov.computed) } else { lookup(&cache.mounted, &cache.computed) } } else { None }; if let Some((msg_opt, at_opt, (ox, oy, rw, rh))) = hit { let msg = if let Some(handler) = at_opt { handler( cursor.x as f32 - ox, cursor.y as f32 - oy, rw, rh, ) } else { msg_opt }; if let Some(msg) = msg { let model = state.model.take().expect("model"); state.model = Some(A::update(model, msg, &self.handle)); state.last_render = None; state.window.request_redraw(); } } } WindowEvent::MouseInput { state: ElementState::Released, button: MouseButton::Left, .. } => { if let Some(drag) = state.drag.take() { let cursor = state.cursor; // 1. Drop: si hay payload + drop target bajo cursor, // invocamos su handler. El Msg resultante se aplica // ANTES del End del drag — la convención es "drop // primero, cleanup del drag después". if let Some(payload) = drag.payload { if let Some(cache) = state.last_render.as_ref() { if let Some(idx) = hit_test_drop( &cache.mounted, &cache.computed, cursor.x as f32, cursor.y as f32, ) { if let Some(drop_h) = cache.mounted.nodes[idx].on_drop.clone() { if let Some(msg) = (drop_h)(payload) { let model = state.model.take().expect("model"); state.model = Some(A::update(model, msg, &self.handle)); } } } } } // 2. Cierre del drag. let end_msg = match &drag.handler { DragHandlerKind::Delta(h) => h(DragPhase::End, 0.0, 0.0), DragHandlerKind::DeltaAt(h, lx0, ly0) => { h(DragPhase::End, 0.0, 0.0, *lx0, *ly0) } }; if let Some(msg) = end_msg { let model = state.model.take().expect("model"); state.model = Some(A::update(model, msg, &self.handle)); } // Cache invalidado siempre — hover/drop pueden cambiar // y el modelo posiblemente mutó. state.last_render = None; state.window.request_redraw(); } } WindowEvent::RedrawRequested => { // Título dinámico (App::window_title): si cambió respecto del // último aplicado, se lo pasamos a winit. Barato: una // comparación de String por frame, set_title sólo en el cambio. if let Some(t) = A::window_title(state.model.as_ref().expect("model")) { if state.last_title.as_deref() != Some(t.as_str()) { state.window.set_title(&t); state.last_title = Some(t); } } // Posicioná la ventana de candidatos del IME junto al caret // (sólo con IME activo y si la app reporta el área). if A::ime_allowed() { if let Some((x, y, w, h)) = A::ime_cursor_area(state.model.as_ref().expect("model")) { state.window.set_ime_cursor_area( PhysicalPosition::new(x as f64, y as f64), llimphi_hal::winit::dpi::PhysicalSize::new( w.max(1.0) as u32, h.max(1.0) as u32, ), ); } } let frame = match state.surface.acquire() { Ok(f) => f, Err(_) => { let (w, h) = state.surface.size(); state.surface.resize(w, h); state.window.request_redraw(); return; } }; let (w, h) = frame.size(); let model_ref = state.model.as_ref().expect("model"); let view = A::view(model_ref); let overlay_view = A::view_overlay(model_ref); // Reusamos los árboles de layout del runtime: `clear()` + // `mount` evita re-allocar el slotmap de taffy por frame. state.layout.clear(); let mounted: Mounted = mount(&mut state.layout, view); let computed = { let ts = &mut state.typesetter; let tmap = &mounted.text_measures; state .layout .compute_with_measure(mounted.root, (w as f32, h as f32), |nid, known, avail| { match tmap.get(&nid) { Some(tm) => measure_text_node(ts, tm, known, avail), None => llimphi_layout::taffy::Size::ZERO, } }) .expect("layout") }; // Mount + layout del overlay en un árbol aparte. Lo // computamos con el mismo tamaño de viewport para que // un scrim a percent(1.0) cubra toda la pantalla. let overlay_built = if let Some(v) = overlay_view { state.overlay_layout.clear(); let omounted: Mounted = mount(&mut state.overlay_layout, v); let ocomputed = { let ts = &mut state.typesetter; let tmap = &omounted.text_measures; state .overlay_layout .compute_with_measure(omounted.root, (w as f32, h as f32), |nid, known, avail| { match tmap.get(&nid) { Some(tm) => measure_text_node(ts, tm, known, avail), None => llimphi_layout::taffy::Size::ZERO, } }) .expect("layout overlay") }; let ohover = hit_test_hover( &omounted, &ocomputed, state.cursor.x as f32, state.cursor.y as f32, ); Some(OverlayCache { mounted: omounted, computed: ocomputed, hover_idx: ohover, }) } else { None }; // Hover en el main solo si NO hay overlay — durante un // menú abierto, el fondo no debe reaccionar al ratón. let hover_idx = if overlay_built.is_some() { None } else { hit_test_hover( &mounted, &computed, state.cursor.x as f32, state.cursor.y as f32, ) }; // Drop hover sólo si hay drag activo con payload (un // drag bloquea el overlay; rara combinación pero la // resolvemos a favor del drag). let drop_hover_idx = state .drag .as_ref() .and_then(|d| d.payload.map(|_| ())) .and_then(|_| { hit_test_drop( &mounted, &computed, state.cursor.x as f32, state.cursor.y as f32, ) }); // Z-order del overlay sobre contenido `gpu_paint`: si el // árbol principal tiene painters gpu (p. ej. el video de // media) Y hay un overlay activo, el overlay NO va en la // escena principal (quedaría debajo del blit gpu). Se // rasteriza aparte sobre fondo transparente y se compone con // alpha DESPUÉS del pase gpu. Sin gpu o sin overlay, el camino // de siempre (overlay en la escena principal) — coste cero. let composite_overlay = overlay_built.is_some() && has_gpu_painter(&mounted); state.scene.reset(); paint( &mut state.scene, &mounted, &computed, &mut state.typesetter, hover_idx, drop_hover_idx, ); if !composite_overlay { if let Some(ov) = overlay_built.as_ref() { paint( &mut state.scene, &ov.mounted, &ov.computed, &mut state.typesetter, ov.hover_idx, None, ); } } if let Err(e) = state.renderer.render( &state.hal, &state.scene, &frame, palette::css::BLACK, ) { eprintln!("render error: {e}"); } let (vw, vh) = frame.size(); // Capa de overlay aparte (camino composite): vello la // rasteriza con fondo transparente en `frame.overlay_view()`. // Se renderiza ANTES del pase gpu para que el blit del // compositor (en `gpu_encoder`) la lea ya escrita. if composite_overlay { if let Some(ov) = overlay_built.as_ref() { state.scene.reset(); paint( &mut state.scene, &ov.mounted, &ov.computed, &mut state.typesetter, ov.hover_idx, None, ); if let Err(e) = state.renderer.render_to_view( &state.hal, &state.scene, frame.overlay_view(), vw, vh, palette::css::TRANSPARENT, ) { eprintln!("render overlay error: {e}"); } } } // Pasada GPU directo (Fase 1 del SDD §"GPU directo wgpu"): // si algún View del main o del overlay registró un // `gpu_painter`, ejecutamos todos sus callbacks contra un // único `CommandEncoder`, encima de lo que vello acaba de // pintar sobre la intermediate. Submitimos antes del // present para que el blit al swapchain incluya las // primitivas GPU. Si nadie usó el hook, no se crea ni // submitea nada — coste cero. let mut gpu_encoder = state.hal.device.create_command_encoder( &llimphi_hal::wgpu::CommandEncoderDescriptor { label: Some("llimphi-ui-gpu-paint"), }, ); let viewport = frame.size(); let mut any_gpu = paint_gpu( &mounted, &computed, &state.hal.device, &state.hal.queue, &mut gpu_encoder, frame.view(), viewport, ); if let Some(ov) = overlay_built.as_ref() { // En el camino composite, los painters gpu del overlay van // sobre SU textura; si no, sobre la intermedia. let target = if composite_overlay { frame.overlay_view() } else { frame.view() }; any_gpu |= paint_gpu( &ov.mounted, &ov.computed, &state.hal.device, &state.hal.queue, &mut gpu_encoder, target, viewport, ); } // Composición alpha del overlay SOBRE la intermedia (que ya // tiene UI + video). Último pase del encoder → corre después // del blit del video. Garantiza menús por encima del video. if composite_overlay { state.overlay_compositor.composite( &state.hal.device, &mut gpu_encoder, frame.view(), frame.overlay_view(), ); any_gpu = true; } if any_gpu { state .hal .queue .submit(std::iter::once(gpu_encoder.finish())); } state.surface.present(frame, &state.hal); state.last_render = Some(RenderCache { mounted, computed, hover_idx, drop_hover_idx, overlay: overlay_built, }); } _ => {} } } } // ── Ventanas secundarias (multiventana, opt-in) ────────────────────────────── // Path APARTE del de la primaria: comparten modelo (vive en `self.state`) y // `Hal`/`Renderer`, pero cada secundaria lleva su surface + caches. Sin // overlay ni foco (la config no los necesita); se puede ampliar luego. impl Runtime { /// Aplica un Msg al modelo (que vive en la primaria) e invalida + repinta /// TODAS las ventanas. Es el camino de cualquier evento de una secundaria, /// así un cambio hecho en la config se refleja al toque en el reproductor /// (y viceversa, vía los ticks que pasan por `user_event`). fn dispatch_model(&mut self, msg: A::Msg) { if let Some(prim) = self.state.as_mut() { let model = prim.model.take().expect("model"); prim.model = Some(A::update(model, msg, &self.handle)); prim.last_render = None; prim.window.request_redraw(); } // OJO: NO repintamos las secundarias acá. `dispatch_model` corre en // cada Msg (incluido el tick ~33 fps), y repintar una secundaria por // tick serializaba dos `acquire()` de swapchain en Wayland FIFO → // ralentización y cuelgue. Cada secundaria se repinta sola al // interactuar con ella (`handle_secondary_event` llama // `render_secondary` tras un cambio) y en su `RedrawRequested` del // compositor (expose/resize). El modelo igual quedó actualizado, así // que el próximo repintado de la secundaria refleja el cambio. } /// Despacha un Msg y repinta la secundaria `idx` en el acto (si sigue /// viva). El camino de los eventos de una secundaria: como su /// `request_redraw` no dispara `RedrawRequested` en algunos compositores, /// la pintamos directo tras el cambio. fn dispatch_and_render_secondary(&mut self, idx: usize, msg: A::Msg) { self.dispatch_model(msg); if idx < self.secondaries.len() { self.render_secondary(idx); } } /// Crea una ventana OS secundaria (o enfoca la existente con esa key). Toma /// el `Hal` de la primaria — no levanta un segundo device GPU. fn open_secondary( &mut self, event_loop: &ActiveEventLoop, key: u64, title: String, width: u32, height: u32, ) { if let Some(sec) = self.secondaries.iter().find(|s| s.key == key) { sec.window.focus_window(); return; } let Some(prim) = self.state.as_ref() else { return; // no hay primaria todavía (no debería pasar) }; let attrs = WindowAttributes::default() .with_title(title) .with_inner_size(LogicalSize::new(width, height)); let window = match event_loop.create_window(attrs) { Ok(w) => Arc::new(w), Err(e) => { eprintln!("open_window: no pude crear la ventana: {e}"); return; } }; let surface = match WinitSurface::new(&prim.hal, window.clone()) { Ok(s) => s, Err(e) => { eprintln!("open_window: no pude crear la surface: {e}"); return; } }; window.request_redraw(); self.secondaries.push(SecondaryState { key, window, surface, scene: vello::Scene::new(), typesetter: llimphi_text::Typesetter::new(), layout: LayoutTree::new(), cursor: PhysicalPosition::new(0.0, 0.0), modifiers: Modifiers::default(), last_render: None, hovered: None, drag: None, last_title: None, }); } /// Pinta la ventana secundaria `idx` con `A::secondary_view`. Reusa el /// `Hal`/`Renderer` de la primaria; camino simple (sin overlay ni /// composite gpu de menús), pero soporta `gpu_paint` por si el contenido /// lo usa. fn render_secondary(&mut self, idx: usize) { let key = self.secondaries[idx].key; let Some(prim) = self.state.as_mut() else { return; }; // Título dinámico de la secundaria. if let Some(t) = A::secondary_title(prim.model.as_ref().expect("model"), key) { let sec = &mut self.secondaries[idx]; if sec.last_title.as_deref() != Some(t.as_str()) { sec.window.set_title(&t); sec.last_title = Some(t); } } let view = A::secondary_view(prim.model.as_ref().expect("model"), key) .unwrap_or_else(|| View::new(Default::default())); let hal = &prim.hal; let renderer = &mut prim.renderer; let sec = &mut self.secondaries[idx]; let frame = match sec.surface.acquire() { Ok(f) => f, Err(_) => { let (w, h) = sec.surface.size(); sec.surface.resize(w, h); sec.window.request_redraw(); return; } }; let (w, h) = frame.size(); sec.layout.clear(); let mounted: Mounted = mount(&mut sec.layout, view); let computed = { let ts = &mut sec.typesetter; let tmap = &mounted.text_measures; sec.layout .compute_with_measure(mounted.root, (w as f32, h as f32), |nid, known, avail| { match tmap.get(&nid) { Some(tm) => measure_text_node(ts, tm, known, avail), None => llimphi_layout::taffy::Size::ZERO, } }) .expect("layout secundario") }; let hover_idx = hit_test_hover(&mounted, &computed, sec.cursor.x as f32, sec.cursor.y as f32); let drop_hover_idx = sec .drag .as_ref() .and_then(|d| d.payload) .and_then(|_| hit_test_drop(&mounted, &computed, sec.cursor.x as f32, sec.cursor.y as f32)); sec.scene.reset(); paint( &mut sec.scene, &mounted, &computed, &mut sec.typesetter, hover_idx, drop_hover_idx, ); if let Err(e) = renderer.render(hal, &sec.scene, &frame, palette::css::BLACK) { eprintln!("render secundario error: {e}"); } // gpu_paint del contenido de la secundaria (si lo hubiera). let mut enc = hal .device .create_command_encoder(&llimphi_hal::wgpu::CommandEncoderDescriptor { label: Some("llimphi-ui-sec-gpu"), }); let viewport = frame.size(); let any = paint_gpu( &mounted, &computed, &hal.device, &hal.queue, &mut enc, frame.view(), viewport, ); if any { hal.queue.submit(std::iter::once(enc.finish())); } sec.surface.present(frame, hal); let _ = (hover_idx, drop_hover_idx); // se usaron al pintar; no se cachean sec.last_render = Some(SecRenderCache { mounted, computed }); } /// Atiende un evento de la ventana secundaria `idx`. Subconjunto de lo que /// hace la primaria (sin overlay/foco/IME): render, resize, cierre, hover, /// click, drag, teclado y rueda — suficiente para un panel de config. fn handle_secondary_event(&mut self, idx: usize, event: WindowEvent) { match event { WindowEvent::CloseRequested => { let key = self.secondaries[idx].key; let msg = self .state .as_ref() .and_then(|p| A::on_secondary_close(p.model.as_ref().expect("model"), key)); self.secondaries.remove(idx); if let Some(msg) = msg { self.dispatch_model(msg); } } WindowEvent::Resized(size) => { self.secondaries[idx].surface.resize(size.width, size.height); self.render_secondary(idx); } WindowEvent::ScaleFactorChanged { .. } => { self.render_secondary(idx); } WindowEvent::RedrawRequested => { self.render_secondary(idx); } WindowEvent::ModifiersChanged(mods) => { self.secondaries[idx].modifiers = mods.state().into(); } WindowEvent::CursorMoved { position, .. } => { let mut drag_msg: Option = None; let mut redraw = false; { let sec = &mut self.secondaries[idx]; sec.cursor = position; if let Some(drag) = sec.drag.as_mut() { let dx = (position.x - drag.last_cursor.x) as f32; let dy = (position.y - drag.last_cursor.y) as f32; drag.last_cursor = position; if dx != 0.0 || dy != 0.0 { drag_msg = match &drag.handler { DragHandlerKind::Delta(h) => h(DragPhase::Move, dx, dy), DragHandlerKind::DeltaAt(h, lx0, ly0) => { h(DragPhase::Move, dx, dy, *lx0, *ly0) } }; } redraw = true; } else { let new_hover = sec.last_render.as_ref().and_then(|c| { hit_test_hover(&c.mounted, &c.computed, position.x as f32, position.y as f32) }); if new_hover != sec.hovered { sec.hovered = new_hover; redraw = true; } } } if let Some(msg) = drag_msg { self.dispatch_and_render_secondary(idx, msg); } else if redraw { self.render_secondary(idx); } } WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left, .. } => { type SecHit = ( Option>, Option>, Option, Option, Option>, Option<(f32, f32, f32, f32)>, ); let cursor = self.secondaries[idx].cursor; let hit: Option> = { let sec = &self.secondaries[idx]; sec.last_render.as_ref().and_then(|c| { hit_test_click(&c.mounted, &c.computed, cursor.x as f32, cursor.y as f32).map( |i| { let node = &c.mounted.nodes[i]; let rect = c.computed.get(node.id).map(|r| (r.x, r.y, r.w, r.h)); ( node.drag.clone(), node.drag_at.clone(), node.drag_payload, node.on_click.clone(), node.on_click_at.clone(), rect, ) }, ) }) }; // Misma prioridad que la primaria: drag_at + on_click_at, luego // drag simple, luego on_click_at, luego on_click. match hit { Some((_, Some(handler_at), payload, _, click_at, Some((ox, oy, rw, rh)))) => { let lx0 = cursor.x as f32 - ox; let ly0 = cursor.y as f32 - oy; if let Some(h) = click_at { if let Some(msg) = h(lx0, ly0, rw, rh) { self.dispatch_model(msg); } } self.secondaries[idx].drag = Some(DragState { handler: DragHandlerKind::DeltaAt(handler_at, lx0, ly0), last_cursor: cursor, payload, }); self.render_secondary(idx); } Some((Some(handler), _, payload, _, _, _)) => { self.secondaries[idx].drag = Some(DragState { handler: DragHandlerKind::Delta(handler), last_cursor: cursor, payload, }); self.render_secondary(idx); } Some((_, _, _, _, Some(handler), Some((ox, oy, rw, rh)))) => { let lx = cursor.x as f32 - ox; let ly = cursor.y as f32 - oy; if let Some(msg) = handler(lx, ly, rw, rh) { self.dispatch_and_render_secondary(idx, msg); } } Some((_, _, _, Some(msg), _, _)) => { self.dispatch_and_render_secondary(idx, msg); } _ => {} } } WindowEvent::MouseInput { state: ElementState::Released, button: MouseButton::Left, .. } => { let cursor = self.secondaries[idx].cursor; let drag = self.secondaries[idx].drag.take(); if let Some(drag) = drag { // Drop primero (si hay payload + target), luego End. if let Some(payload) = drag.payload { let drop_h = self.secondaries[idx].last_render.as_ref().and_then(|c| { hit_test_drop(&c.mounted, &c.computed, cursor.x as f32, cursor.y as f32) .and_then(|i| c.mounted.nodes[i].on_drop.clone()) }); if let Some(h) = drop_h { if let Some(msg) = h(payload) { self.dispatch_model(msg); } } } let end_msg = match &drag.handler { DragHandlerKind::Delta(h) => h(DragPhase::End, 0.0, 0.0), DragHandlerKind::DeltaAt(h, lx0, ly0) => h(DragPhase::End, 0.0, 0.0, *lx0, *ly0), }; if let Some(msg) = end_msg { self.dispatch_model(msg); } self.render_secondary(idx); } } WindowEvent::MouseWheel { delta, .. } => { let wd = match delta { MouseScrollDelta::LineDelta(x, y) => WheelDelta { x, y: -y }, MouseScrollDelta::PixelDelta(p) => WheelDelta { x: (p.x as f32) / 20.0, y: -(p.y as f32) / 20.0, }, }; let cursor = self.secondaries[idx].cursor; let handler = { let sec = &self.secondaries[idx]; sec.last_render.as_ref().and_then(|c| { hit_test_scroll(&c.mounted, &c.computed, cursor.x as f32, cursor.y as f32) .and_then(|i| c.mounted.nodes[i].on_scroll.clone()) }) }; if let Some(msg) = handler.and_then(|h| h(wd.x, wd.y)) { self.dispatch_and_render_secondary(idx, msg); } } WindowEvent::KeyboardInput { event, .. } => { let ev = KeyEvent { key: event.logical_key.clone(), state: match event.state { ElementState::Pressed => KeyState::Pressed, ElementState::Released => KeyState::Released, }, text: event.text.as_ref().map(|t| t.to_string()), modifiers: self.secondaries[idx].modifiers, repeat: event.repeat, }; let msg = self .state .as_ref() .and_then(|p| A::on_key(p.model.as_ref().expect("model"), &ev)); if let Some(msg) = msg { self.dispatch_and_render_secondary(idx, msg); } } _ => {} } } }