Skip to main content

cvkg_render_native/
lib.rs

1//! # CVKG Agentic Development Guidelines (v1.2)
2//!
3//! All AI agents contributing to this crate MUST follow ALL seven rules:
4//!
5//! ── Karpathy Guidelines (1–4) ────────────────────────────────────────────
6//! 1. THINK FIRST     — State assumptions. Surface ambiguity. Push back on complexity.
7//! 2. STAY SIMPLE     — Minimum code. No speculative features. No unasked-for abstractions.
8//! 3. BE SURGICAL     — Touch only what's required. Own your orphans. Don't improve neighbors.
9//! 4. VERIFY GOALS    — Turn tasks into checkable criteria. Loop until they pass. Never commit broken.
10//!
11//! ── CVKG Extended Protocols (5–7) ────────────────────────────────────────
12//! 5. TRIPLE-PASS     — Read the target, its surrounding context, and its full call graph
13//                      at least THREE TIMES before making any edit or revision.
14//! 6. COMMENT ALL     — Every major pub fn, unsafe block, and non-trivial algorithm in
15//                      every .rs/.ts/.h/.wgsl file MUST have a descriptive doc comment.
16//                      Comments describe WHY and WHAT CONTRACT, not HOW mechanically.
17//! 7. MONITOR LOOPS   — Check every tool call / command for progress every 30 seconds.
18//                      After 3 consecutive identical failures, stop, write BLOCKED.md,
19//                      and move to unblocked work. Never silently accept a broken state.
20//!
21//! Sources:
22//   Karpathy: https://github.com/multica-ai/andrej-karpathy-skills
23//   CVKG Extended: Section 2 of the CVKG Design Specification
24
25//! Platform-native widget delegation using winit and AccessKit
26//!
27//! This crate provides platform-specific rendering backends for native desktop targets
28//  using winit for window/event handling and AccessKit for accessibility tree integration.
29
30use std::sync::Arc;
31use winit::{
32    application::ApplicationHandler,
33    event::WindowEvent,
34    event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
35    window::{Window, WindowId},
36};
37
38/// Native renderer backend implementing the Renderer trait.
39/// It wraps a SurtrRenderer for high-performance GPU drawing.
40pub struct NativeRenderer {
41    window: Arc<Window>,
42    gpu: Option<cvkg_render_gpu::SurtrRenderer>,
43}
44
45/// Custom events for the native application event loop
46#[derive(Debug)]
47enum AppEvent {
48    AccessibilityAction(accesskit::ActionRequest),
49}
50
51impl NativeRenderer {
52    /// Create a new NativeRenderer (internal use by App)
53    fn new(window: Arc<Window>) -> Self {
54        Self { window, gpu: None }
55    }
56
57    /// Start the CVKG native application with the given view.
58    /// This is the main entry point for desktop applications.
59    pub fn run<V: cvkg_core::View + 'static>(view: V) {
60        let event_loop = EventLoop::<AppEvent>::with_user_event()
61            .build()
62            .expect("Failed to create event loop");
63        event_loop.set_control_flow(ControlFlow::Poll);
64
65        let mut app = App {
66            view,
67            renderer: None,
68            accesskit_adapter: None,
69            vdom: Some(cvkg_vdom::VDom::new()),
70            asset_manager: std::sync::Arc::new(NativeAssetManager::new()),
71            cursor_pos: [0.0, 0.0],
72            proxy: event_loop.create_proxy(),
73        };
74
75        event_loop.run_app(&mut app).expect("Event loop error");
76    }
77}
78
79struct App<V: cvkg_core::View> {
80    view: V,
81    renderer: Option<NativeRenderer>,
82    accesskit_adapter: Option<accesskit_winit::Adapter>,
83    vdom: Option<cvkg_vdom::VDom>,
84    asset_manager: std::sync::Arc<NativeAssetManager>,
85    cursor_pos: [f32; 2],
86    proxy: winit::event_loop::EventLoopProxy<AppEvent>,
87}
88
89impl<V: cvkg_core::View + 'static> ApplicationHandler<AppEvent> for App<V> {
90    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
91        let window_attrs = Window::default_attributes()
92            .with_title("CVKG Forge")
93            .with_inner_size(winit::dpi::LogicalSize::new(1280.0, 720.0));
94
95        let window = Arc::new(
96            event_loop
97                .create_window(window_attrs)
98                .expect("Failed to create window"),
99        );
100
101        // Initialize AccessKit adapter
102        let adapter = accesskit_winit::Adapter::with_direct_handlers(
103            event_loop,
104            &window,
105            ShieldWall {
106                proxy: self.proxy.clone(),
107            },
108            ShieldWall {
109                proxy: self.proxy.clone(),
110            },
111            ShieldWall {
112                proxy: self.proxy.clone(),
113            },
114        );
115        self.accesskit_adapter = Some(adapter);
116
117        let mut renderer = NativeRenderer::new(window.clone());
118
119        // Use a Runtime to block on the async forge process
120        let rt = tokio::runtime::Runtime::new().unwrap();
121        renderer.gpu = Some(rt.block_on(cvkg_render_gpu::SurtrRenderer::forge(window.clone())));
122
123        self.renderer = Some(renderer);
124
125        // Register AssetManager in the environment
126        cvkg_core::env::insert::<cvkg_core::AssetKey>(self.asset_manager.clone());
127    }
128
129    fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
130        let renderer = if let Some(r) = &mut self.renderer {
131            r
132        } else {
133            return;
134        };
135
136        match event {
137            WindowEvent::CloseRequested => event_loop.exit(),
138            WindowEvent::Resized(physical_size) => {
139                if let Some(gpu) = &mut renderer.gpu {
140                    gpu.resize(
141                        physical_size.width,
142                        physical_size.height,
143                        renderer.window.scale_factor() as f32,
144                    );
145                }
146                renderer.window.request_redraw();
147            }
148            WindowEvent::RedrawRequested => {
149                if let Some(gpu) = &mut renderer.gpu {
150                    let encoder = gpu.begin_frame();
151
152                    let size = renderer.window.inner_size();
153                    let scale = renderer.window.scale_factor();
154                    let logical_size = size.to_logical::<f32>(scale);
155                    
156                    let rect = cvkg_core::Rect {
157                        x: 0.0,
158                        y: 0.0,
159                        width: logical_size.width,
160                        height: logical_size.height,
161                    };
162
163                    // Render the view tree
164                    self.view.render(gpu, rect);
165
166                    // Update VDOM and Accessibility Tree
167                    let new_vdom = cvkg_vdom::VDom::build(&self.view, rect);
168                    if let Some(prev_vdom) = &mut self.vdom {
169                        let patches = prev_vdom.diff(&new_vdom);
170
171                        // Generate AccessKit updates from patches if adapter is available
172                        if let Some(adapter) = &mut self.accesskit_adapter {
173                            let mut nodes = Vec::new();
174                            for patch in &patches {
175                                match patch {
176                                    cvkg_vdom::VDomPatch::Create(node)
177                                    | cvkg_vdom::VDomPatch::Replace { node, .. } => {
178                                        nodes.push((
179                                            accesskit::NodeId(node.id.0 as u64),
180                                            node.to_accesskit_node(),
181                                        ));
182                                    }
183                                    cvkg_vdom::VDomPatch::Update { id, .. } => {
184                                        if let Some(node) = new_vdom.nodes.get(id) {
185                                            nodes.push((
186                                                accesskit::NodeId(node.id.0 as u64),
187                                                node.to_accesskit_node(),
188                                            ));
189                                        }
190                                    }
191                                    _ => {}
192                                }
193                            }
194
195                            if !nodes.is_empty() {
196                                adapter.update_if_active(|| accesskit::TreeUpdate {
197                                    nodes,
198                                    tree: None,
199                                    focus: accesskit::NodeId(1),
200                                });
201                            }
202                        }
203
204                        // Apply patches to preserve stateful properties (capture, focus)
205                        prev_vdom.apply_patches(patches);
206                    } else {
207                        self.vdom = Some(new_vdom);
208                    }
209
210                    gpu.end_frame(encoder);
211                }
212            }
213            WindowEvent::CursorMoved { position, .. } => {
214                let scale = renderer.window.scale_factor();
215                let logical = position.to_logical::<f32>(scale);
216                self.cursor_pos = [logical.x, logical.y];
217                if let Some(vdom) = &self.vdom {
218                    vdom.dispatch_event(cvkg_core::Event::PointerMove {
219                        x: self.cursor_pos[0],
220                        y: self.cursor_pos[1],
221                    });
222                }
223            }
224            WindowEvent::MouseInput { state, .. } => {
225                if let Some(vdom) = &self.vdom {
226                    let event = match state {
227                        winit::event::ElementState::Pressed => {
228                            let id = vdom.hit_test(self.cursor_pos[0], self.cursor_pos[1]);
229                            println!("Native: MousePress at {:?}, hit={:?}", self.cursor_pos, id);
230                            cvkg_core::Event::PointerDown {
231                                x: self.cursor_pos[0],
232                                y: self.cursor_pos[1],
233                            }
234                        }
235                        winit::event::ElementState::Released => cvkg_core::Event::PointerUp {
236                            x: self.cursor_pos[0],
237                            y: self.cursor_pos[1],
238                        },
239                    };
240                    vdom.dispatch_event(event);
241                }
242            }
243            WindowEvent::KeyboardInput { event, .. } => {
244                if let Some(vdom) = &self.vdom {
245                    if let winit::keyboard::PhysicalKey::Code(code) = event.physical_key {
246                        let key_str = format!("{:?}", code);
247                        let cvkg_event = if event.state == winit::event::ElementState::Pressed {
248                            cvkg_core::Event::KeyDown { key: key_str }
249                        } else {
250                            cvkg_core::Event::KeyUp { key: key_str }
251                        };
252                        vdom.dispatch_event(cvkg_event);
253                    }
254
255                    // Also handle text input (IME / character events)
256                    if event.state == winit::event::ElementState::Pressed {
257                        if let Some(text) = event.text {
258                            for c in text.chars() {
259                                vdom.dispatch_event(cvkg_core::Event::KeyDown {
260                                    key: c.to_string(),
261                                });
262                            }
263                        }
264                    }
265                }
266            }
267            _ => (),
268        }
269    }
270
271    fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: AppEvent) {
272        match event {
273            AppEvent::AccessibilityAction(request) => {
274                if let Some(vdom) = &self.vdom {
275                    let node_id = cvkg_vdom::NodeId(request.target.0 as usize);
276                    if let Some(node) = vdom.nodes.get(&node_id) {
277                        match request.action {
278                            accesskit::Action::Click => {
279                                // Translate default action (click) to PointerClick
280                                let event = cvkg_core::Event::PointerClick {
281                                    x: node.layout.x + node.layout.width / 2.0,
282                                    y: node.layout.y + node.layout.height / 2.0,
283                                };
284                                vdom.dispatch_event(event);
285                            }
286                            accesskit::Action::Focus => {
287                                if let Ok(mut focus) = vdom.focused_node.lock() {
288                                    *focus = Some(node_id);
289                                }
290                            }
291                            _ => (),
292                        }
293                    }
294                }
295            }
296        }
297    }
298
299    fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
300        if let Some(renderer) = &self.renderer {
301            renderer.window.request_redraw();
302        }
303    }
304}
305
306impl cvkg_core::Renderer for NativeRenderer {
307    fn fill_rect(&mut self, rect: cvkg_core::Rect, color: [f32; 4]) {
308        if let Some(gpu) = &mut self.gpu {
309            gpu.fill_rect(rect, color);
310        }
311    }
312    fn fill_rounded_rect(&mut self, rect: cvkg_core::Rect, radius: f32, color: [f32; 4]) {
313        if let Some(gpu) = &mut self.gpu {
314            gpu.fill_rounded_rect(rect, radius, color);
315        }
316    }
317    fn fill_ellipse(&mut self, rect: cvkg_core::Rect, color: [f32; 4]) {
318        if let Some(gpu) = &mut self.gpu {
319            gpu.fill_ellipse(rect, color);
320        }
321    }
322    fn stroke_rect(&mut self, rect: cvkg_core::Rect, color: [f32; 4], stroke_width: f32) {
323        if let Some(gpu) = &mut self.gpu {
324            gpu.stroke_rect(rect, color, stroke_width);
325        }
326    }
327    fn stroke_rounded_rect(
328        &mut self,
329        rect: cvkg_core::Rect,
330        radius: f32,
331        color: [f32; 4],
332        stroke_width: f32,
333    ) {
334        if let Some(gpu) = &mut self.gpu {
335            gpu.stroke_rounded_rect(rect, radius, color, stroke_width);
336        }
337    }
338    fn stroke_ellipse(&mut self, rect: cvkg_core::Rect, color: [f32; 4], stroke_width: f32) {
339        if let Some(gpu) = &mut self.gpu {
340            gpu.stroke_ellipse(rect, color, stroke_width);
341        }
342    }
343    fn draw_line(
344        &mut self,
345        x1: f32,
346        y1: f32,
347        x2: f32,
348        y2: f32,
349        color: [f32; 4],
350        stroke_width: f32,
351    ) {
352        if let Some(gpu) = &mut self.gpu {
353            gpu.draw_line(x1, y1, x2, y2, color, stroke_width);
354        }
355    }
356    fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
357        if let Some(gpu) = &mut self.gpu {
358            gpu.draw_text(text, x, y, size, color);
359        }
360    }
361    fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
362        if let Some(gpu) = &mut self.gpu {
363            gpu.measure_text(text, size)
364        } else {
365            (0.0, 0.0)
366        }
367    }
368    fn draw_texture(&mut self, texture_id: u32, rect: cvkg_core::Rect) {
369        if let Some(gpu) = &mut self.gpu {
370            gpu.draw_texture(texture_id, rect);
371        }
372    }
373    fn draw_image(&mut self, image_name: &str, rect: cvkg_core::Rect) {
374        if let Some(gpu) = &mut self.gpu {
375            gpu.draw_image(image_name, rect);
376        }
377    }
378    fn load_image(&mut self, name: &str, data: &[u8]) {
379        if let Some(gpu) = &mut self.gpu {
380            gpu.load_image(name, data);
381        }
382    }
383    fn push_clip_rect(&mut self, rect: cvkg_core::Rect) {
384        if let Some(gpu) = &mut self.gpu {
385            gpu.push_clip_rect(rect);
386        }
387    }
388    fn pop_clip_rect(&mut self) {
389        if let Some(gpu) = &mut self.gpu {
390            gpu.pop_clip_rect();
391        }
392    }
393    fn push_opacity(&mut self, opacity: f32) {
394        if let Some(gpu) = &mut self.gpu {
395            gpu.push_opacity(opacity);
396        }
397    }
398    fn pop_opacity(&mut self) {
399        if let Some(gpu) = &mut self.gpu {
400            gpu.pop_opacity();
401        }
402    }
403    fn bifrost(&mut self, rect: cvkg_core::Rect, blur: f32, saturation: f32, opacity: f32) {
404        if let Some(gpu) = &mut self.gpu {
405            gpu.bifrost(rect, blur, saturation, opacity);
406        }
407    }
408    fn push_mjolnir_slice(&mut self, angle: f32, offset: f32) {
409        if let Some(gpu) = &mut self.gpu {
410            gpu.push_mjolnir_slice(angle, offset);
411        }
412    }
413    fn pop_mjolnir_slice(&mut self) {
414        if let Some(gpu) = &mut self.gpu {
415            gpu.pop_mjolnir_slice();
416        }
417    }
418    fn register_shared_element(&mut self, id: &str, rect: cvkg_core::Rect) {
419        if let Some(gpu) = &mut self.gpu {
420            gpu.register_shared_element(id, rect);
421        }
422    }
423}
424
425
426
427// Platform-specific implementations for macOS, Windows, and Linux are handled by winit and AccessKit.
428
429struct ShieldWall {
430    proxy: winit::event_loop::EventLoopProxy<AppEvent>,
431}
432
433impl accesskit::ActionHandler for ShieldWall {
434    fn do_action(&mut self, request: accesskit::ActionRequest) {
435        let _ = self.proxy.send_event(AppEvent::AccessibilityAction(request));
436    }
437}
438
439impl accesskit::ActivationHandler for ShieldWall {
440    fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
441        let mut root = accesskit::Node::new(accesskit::Role::Window);
442        root.set_label("CVKG Application");
443
444        let root_id = accesskit::NodeId(1);
445        Some(accesskit::TreeUpdate {
446            nodes: vec![(root_id, root)],
447            tree: Some(accesskit::Tree::new(root_id)),
448            focus: root_id,
449        })
450    }
451}
452
453impl accesskit::DeactivationHandler for ShieldWall {
454    fn deactivate_accessibility(&mut self) {}
455}
456
457/// A concrete AssetManager for native desktop targets that loads from the local filesystem.
458pub struct NativeAssetManager {
459    cache: std::sync::Arc<
460        std::sync::RwLock<
461            std::collections::HashMap<String, cvkg_core::AssetState<std::sync::Arc<Vec<u8>>>>,
462        >,
463    >,
464}
465
466impl NativeAssetManager {
467    pub fn new() -> Self {
468        Self {
469            cache: std::sync::Arc::new(std::sync::RwLock::new(std::collections::HashMap::new())),
470        }
471    }
472}
473
474impl cvkg_core::AssetManager for NativeAssetManager {
475    fn load_image(&self, url: &str) -> cvkg_core::AssetState<std::sync::Arc<Vec<u8>>> {
476        {
477            let cache = self.cache.read().unwrap();
478            if let Some(state) = cache.get(url) {
479                return state.clone();
480            }
481        }
482
483        // Real filesystem I/O (simplistic implementation for now)
484        match std::fs::read(url) {
485            Ok(data) => {
486                let state = cvkg_core::AssetState::Ready(std::sync::Arc::new(data));
487                let mut cache = self.cache.write().unwrap();
488                cache.insert(url.to_string(), state.clone());
489                state
490            }
491            Err(e) => {
492                let state = cvkg_core::AssetState::Error(e.to_string());
493                let mut cache = self.cache.write().unwrap();
494                cache.insert(url.to_string(), state.clone());
495                state
496            }
497        }
498    }
499
500    fn preload_image(&self, _url: &str) {
501        // Implementation for async preloading could go here
502    }
503}