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 shared SurtrRenderer for high-performance GPU drawing.
40pub struct NativeRenderer {
41    gpu: Arc<std::sync::Mutex<cvkg_render_gpu::SurtrRenderer>>,
42}
43
44/// Custom events for the native application event loop
45#[derive(Debug)]
46enum AppEvent {
47    AccessibilityAction(accesskit::ActionRequest),
48}
49
50impl NativeRenderer {
51    /// Create a new NativeRenderer (internal use by App)
52    fn new(_window: Arc<Window>, gpu: Arc<std::sync::Mutex<cvkg_render_gpu::SurtrRenderer>>) -> Self {
53        Self { gpu }
54    }
55
56    /// Start the CVKG native application with the given view.
57    /// This is the main entry point for desktop applications.
58    pub fn run<V: cvkg_core::View + 'static>(view: V) {
59        let event_loop = EventLoop::<AppEvent>::with_user_event()
60            .build()
61            .expect("Failed to create event loop");
62        event_loop.set_control_flow(ControlFlow::Poll);
63
64        let mut app = App {
65            view,
66            windows: std::collections::HashMap::new(),
67            gpu: None,
68            asset_manager: std::sync::Arc::new(NativeAssetManager::new()),
69            proxy: event_loop.create_proxy(),
70        };
71
72        event_loop.run_app(&mut app).expect("Event loop error");
73    }
74}
75
76struct WindowState {
77    window: Arc<Window>,
78    accesskit_adapter: Option<accesskit_winit::Adapter>,
79    vdom: Option<cvkg_vdom::VDom>,
80    cursor_pos: [f32; 2],
81}
82
83struct App<V: cvkg_core::View> {
84    view: V,
85    windows: std::collections::HashMap<WindowId, WindowState>,
86    gpu: Option<Arc<std::sync::Mutex<cvkg_render_gpu::SurtrRenderer>>>,
87    asset_manager: std::sync::Arc<NativeAssetManager>,
88    proxy: winit::event_loop::EventLoopProxy<AppEvent>,
89}
90
91impl<V: cvkg_core::View + 'static> ApplicationHandler<AppEvent> for App<V> {
92    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
93        if self.gpu.is_none() {
94            let window_attrs = Window::default_attributes()
95                .with_title("CVKG Forge")
96                .with_inner_size(winit::dpi::LogicalSize::new(1280.0, 720.0));
97
98            let window = Arc::new(
99                event_loop
100                    .create_window(window_attrs)
101                    .expect("Failed to create window"),
102            );
103            window.set_ime_allowed(true);
104
105            let adapter = accesskit_winit::Adapter::with_direct_handlers(
106                event_loop,
107                &window,
108                ShieldWall { proxy: self.proxy.clone() },
109                ShieldWall { proxy: self.proxy.clone() },
110                ShieldWall { proxy: self.proxy.clone() },
111            );
112
113            let rt = tokio::runtime::Runtime::new().unwrap();
114            let gpu = rt.block_on(cvkg_render_gpu::SurtrRenderer::forge(window.clone()));
115            let gpu = Arc::new(std::sync::Mutex::new(gpu));
116            self.gpu = Some(gpu);
117
118            self.windows.insert(window.id(), WindowState {
119                window,
120                accesskit_adapter: Some(adapter),
121                vdom: Some(cvkg_vdom::VDom::new()),
122                cursor_pos: [0.0, 0.0],
123            });
124
125            cvkg_core::env::insert::<cvkg_core::AssetKey>(self.asset_manager.clone());
126        }
127    }
128
129    fn window_event(&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
130        let gpu_arc = if let Some(g) = &self.gpu { g.clone() } else { return };
131        let state = if let Some(s) = self.windows.get_mut(&id) { s } else { return };
132
133        match event {
134            WindowEvent::CloseRequested => {
135                self.windows.remove(&id);
136                if self.windows.is_empty() {
137                    event_loop.exit();
138                }
139            }
140            WindowEvent::Resized(physical_size) => {
141                gpu_arc.lock().unwrap().resize(
142                    id,
143                    physical_size.width,
144                    physical_size.height,
145                    state.window.scale_factor() as f32,
146                );
147                state.window.request_redraw();
148            }
149            WindowEvent::RedrawRequested => {
150                let size = state.window.inner_size();
151                let scale = state.window.scale_factor();
152                let logical_size = size.to_logical::<f32>(scale);
153
154                let rect = cvkg_core::Rect {
155                    x: 0.0,
156                    y: 0.0,
157                    width: logical_size.width,
158                    height: logical_size.height,
159                };
160
161                let new_vdom = cvkg_vdom::VDom::build(&self.view, rect);
162                if let Some(prev_vdom) = &mut state.vdom {
163                    let patches = prev_vdom.diff(&new_vdom);
164                    if let Some(adapter) = &mut state.accesskit_adapter {
165                        let mut nodes = Vec::new();
166                        for patch in &patches {
167                            match patch {
168                                cvkg_vdom::VDomPatch::Create(node)
169                                | cvkg_vdom::VDomPatch::Replace { node, .. } => {
170                                    nodes.push((accesskit::NodeId(node.id.0 as u64), node.to_accesskit_node()));
171                                }
172                                cvkg_vdom::VDomPatch::Update { id, .. } => {
173                                    if let Some(node) = new_vdom.nodes.get(id) {
174                                        nodes.push((accesskit::NodeId(node.id.0 as u64), node.to_accesskit_node()));
175                                    }
176                                }
177                                _ => {}
178                            }
179                        }
180                        if !nodes.is_empty() {
181                            adapter.update_if_active(|| accesskit::TreeUpdate {
182                                nodes,
183                                tree: None,
184                                focus: accesskit::NodeId(1),
185                            });
186                        }
187                    }
188                    prev_vdom.apply_patches(patches);
189                } else {
190                    state.vdom = Some(new_vdom);
191                }
192
193                {
194                    let mut gpu = gpu_arc.lock().unwrap();
195                    let encoder = gpu.begin_frame(id);
196                    let mut renderer = NativeRenderer::new(state.window.clone(), gpu_arc.clone());
197                    self.view.render(&mut renderer, rect);
198                    gpu.end_frame(encoder);
199                }
200            }
201            WindowEvent::CursorMoved { position, .. } => {
202                let scale = state.window.scale_factor();
203                let logical = position.to_logical::<f32>(scale);
204                state.cursor_pos = [logical.x, logical.y];
205                if let Some(vdom) = &state.vdom {
206                    vdom.dispatch_event(cvkg_core::Event::PointerMove {
207                        x: state.cursor_pos[0],
208                        y: state.cursor_pos[1],
209                    });
210                }
211            }
212            WindowEvent::MouseInput { state: mouse_state, .. } => {
213                if let Some(vdom) = &state.vdom {
214                    let event = match mouse_state {
215                        winit::event::ElementState::Pressed => {
216                            cvkg_core::Event::PointerDown {
217                                x: state.cursor_pos[0],
218                                y: state.cursor_pos[1],
219                            }
220                        }
221                        winit::event::ElementState::Released => cvkg_core::Event::PointerUp {
222                            x: state.cursor_pos[0],
223                            y: state.cursor_pos[1],
224                        },
225                    };
226                    vdom.dispatch_event(event);
227                }
228            }
229            WindowEvent::KeyboardInput { event, .. } => {
230                if let Some(vdom) = &state.vdom {
231                    if let winit::keyboard::PhysicalKey::Code(code) = event.physical_key {
232                        let key_str = format!("{:?}", code);
233                        let cvkg_event = if event.state == winit::event::ElementState::Pressed {
234                            cvkg_core::Event::KeyDown { key: key_str }
235                        } else {
236                            cvkg_core::Event::KeyUp { key: key_str }
237                        };
238                        vdom.dispatch_event(cvkg_event);
239                    }
240                }
241            }
242            WindowEvent::Ime(ime_event) => {
243                if let Some(vdom) = &state.vdom {
244                    match ime_event {
245                        winit::event::Ime::Commit(string) => {
246                            vdom.dispatch_event(cvkg_core::Event::Ime(string));
247                        }
248                        _ => {}
249                    }
250                }
251            }
252            _ => {}
253        }
254    }
255
256    fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: AppEvent) {
257        match event {
258            AppEvent::AccessibilityAction(request) => {
259                let node_id = cvkg_vdom::NodeId(request.target.0 as u64);
260                // For accessibility, we'll route to the first window for now
261                if let Some(state) = self.windows.values_mut().next() {
262                    if let Some(vdom) = &state.vdom {
263                        if let Some(node) = vdom.nodes.get(&node_id) {
264                            match request.action {
265                                accesskit::Action::Click => {
266                                    let event = cvkg_core::Event::PointerClick {
267                                        x: node.layout.x + node.layout.width / 2.0,
268                                        y: node.layout.y + node.layout.height / 2.0,
269                                    };
270                                    vdom.dispatch_event(event);
271                                }
272                                _ => ()
273                            }
274                        }
275                    }
276                }
277            }
278        }
279    }
280
281    fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
282        for state in self.windows.values() {
283            state.window.request_redraw();
284        }
285    }
286}
287
288impl cvkg_core::Renderer for NativeRenderer {
289    fn fill_rect(&mut self, rect: cvkg_core::Rect, color: [f32; 4]) {
290        self.gpu.lock().unwrap().fill_rect(rect, color);
291    }
292    fn fill_rounded_rect(&mut self, rect: cvkg_core::Rect, radius: f32, color: [f32; 4]) {
293        self.gpu.lock().unwrap().fill_rounded_rect(rect, radius, color);
294    }
295    fn fill_ellipse(&mut self, rect: cvkg_core::Rect, color: [f32; 4]) {
296        self.gpu.lock().unwrap().fill_ellipse(rect, color);
297    }
298    fn stroke_rect(&mut self, rect: cvkg_core::Rect, color: [f32; 4], stroke_width: f32) {
299        self.gpu.lock().unwrap().stroke_rect(rect, color, stroke_width);
300    }
301    fn stroke_rounded_rect(
302        &mut self,
303        rect: cvkg_core::Rect,
304        radius: f32,
305        color: [f32; 4],
306        stroke_width: f32,
307    ) {
308        self.gpu.lock().unwrap().stroke_rounded_rect(rect, radius, color, stroke_width);
309    }
310    fn stroke_ellipse(&mut self, rect: cvkg_core::Rect, color: [f32; 4], stroke_width: f32) {
311        self.gpu.lock().unwrap().stroke_ellipse(rect, color, stroke_width);
312    }
313    fn draw_line(
314        &mut self,
315        x1: f32,
316        y1: f32,
317        x2: f32,
318        y2: f32,
319        color: [f32; 4],
320        stroke_width: f32,
321    ) {
322        self.gpu.lock().unwrap().draw_line(x1, y1, x2, y2, color, stroke_width);
323    }
324    fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
325        self.gpu.lock().unwrap().draw_text(text, x, y, size, color);
326    }
327    fn measure_text(&mut self, text: &str, size: f32) -> (f32, f32) {
328        self.gpu.lock().unwrap().measure_text(text, size)
329    }
330    fn draw_texture(&mut self, texture_id: u32, rect: cvkg_core::Rect) {
331        self.gpu.lock().unwrap().draw_texture(texture_id, rect);
332    }
333    fn draw_image(&mut self, image_name: &str, rect: cvkg_core::Rect) {
334        self.gpu.lock().unwrap().draw_image(image_name, rect);
335    }
336    fn load_image(&mut self, name: &str, data: &[u8]) {
337        self.gpu.lock().unwrap().load_image(name, data);
338    }
339    fn push_clip_rect(&mut self, rect: cvkg_core::Rect) {
340        self.gpu.lock().unwrap().push_clip_rect(rect);
341    }
342    fn pop_clip_rect(&mut self) {
343        self.gpu.lock().unwrap().pop_clip_rect();
344    }
345    fn push_opacity(&mut self, opacity: f32) {
346        self.gpu.lock().unwrap().push_opacity(opacity);
347    }
348    fn pop_opacity(&mut self) {
349        self.gpu.lock().unwrap().pop_opacity();
350    }
351    fn bifrost(&mut self, rect: cvkg_core::Rect, blur: f32, saturation: f32, opacity: f32) {
352        self.gpu.lock().unwrap().bifrost(rect, blur, saturation, opacity);
353    }
354    fn push_mjolnir_slice(&mut self, angle: f32, offset: f32) {
355        self.gpu.lock().unwrap().push_mjolnir_slice(angle, offset);
356    }
357    fn pop_mjolnir_slice(&mut self) {
358        self.gpu.lock().unwrap().pop_mjolnir_slice();
359    }
360    fn register_shared_element(&mut self, id: &str, rect: cvkg_core::Rect) {
361        self.gpu.lock().unwrap().register_shared_element(id, rect);
362    }
363    fn set_z_index(&mut self, z: f32) {
364        self.gpu.lock().unwrap().set_z_index(z);
365    }
366    fn get_z_index(&self) -> f32 {
367        self.gpu.lock().unwrap().get_z_index()
368    }
369    fn load_svg(&mut self, name: &str, svg_data: &[u8]) {
370        self.gpu.lock().unwrap().load_svg(name, svg_data);
371    }
372    fn draw_svg(&mut self, name: &str, rect: cvkg_core::Rect) {
373        self.gpu.lock().unwrap().draw_svg(name, rect, None, 0);
374    }
375    fn get_telemetry(&self) -> cvkg_core::TelemetryData {
376        self.gpu.lock().unwrap().telemetry.clone()
377    }
378}
379
380// Platform-specific implementations for macOS, Windows, and Linux are handled by winit and AccessKit.
381
382struct ShieldWall {
383    proxy: winit::event_loop::EventLoopProxy<AppEvent>,
384}
385
386impl accesskit::ActionHandler for ShieldWall {
387    fn do_action(&mut self, request: accesskit::ActionRequest) {
388        let _ = self
389            .proxy
390            .send_event(AppEvent::AccessibilityAction(request));
391    }
392}
393
394impl accesskit::ActivationHandler for ShieldWall {
395    fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
396        let mut root = accesskit::Node::new(accesskit::Role::Window);
397        root.set_label("CVKG Application");
398
399        let root_id = accesskit::NodeId(1);
400        Some(accesskit::TreeUpdate {
401            nodes: vec![(root_id, root)],
402            tree: Some(accesskit::Tree::new(root_id)),
403            focus: root_id,
404        })
405    }
406}
407
408impl accesskit::DeactivationHandler for ShieldWall {
409    fn deactivate_accessibility(&mut self) {}
410}
411
412/// A concrete AssetManager for native desktop targets that loads from the local filesystem.
413pub struct NativeAssetManager {
414    cache: std::sync::Arc<
415        std::sync::RwLock<
416            std::collections::HashMap<String, cvkg_core::AssetState<std::sync::Arc<Vec<u8>>>>,
417        >,
418    >,
419}
420
421impl NativeAssetManager {
422    pub fn new() -> Self {
423        Self {
424            cache: std::sync::Arc::new(std::sync::RwLock::new(std::collections::HashMap::new())),
425        }
426    }
427}
428
429impl cvkg_core::AssetManager for NativeAssetManager {
430    fn load_image(&self, url: &str) -> cvkg_core::AssetState<std::sync::Arc<Vec<u8>>> {
431        {
432            let cache = self.cache.read().unwrap();
433            if let Some(state) = cache.get(url) {
434                return state.clone();
435            }
436        }
437
438        // Real filesystem I/O (simplistic implementation for now)
439        match std::fs::read(url) {
440            Ok(data) => {
441                let state = cvkg_core::AssetState::Ready(std::sync::Arc::new(data));
442                let mut cache = self.cache.write().unwrap();
443                cache.insert(url.to_string(), state.clone());
444                state
445            }
446            Err(e) => {
447                let state = cvkg_core::AssetState::Error(e.to_string());
448                let mut cache = self.cache.write().unwrap();
449                cache.insert(url.to_string(), state.clone());
450                state
451            }
452        }
453    }
454
455    fn preload_image(&self, _url: &str) {
456        // Implementation for async preloading could go here
457    }
458}