Skip to main content

forge_ui/
window.rs

1use crate::*;
2use serde::Deserialize;
3use serde_json::{json, Value};
4use softbuffer::{Context, Surface};
5use std::{
6    error::Error,
7    io::{BufRead, Read},
8    num::NonZeroU32,
9    rc::Rc,
10};
11use winit::{
12    application::ApplicationHandler,
13    dpi::LogicalSize,
14    event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent},
15    event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
16    keyboard::{Key, ModifiersState, NamedKey},
17    window::{Window, WindowId},
18};
19
20#[derive(Clone, Copy, Debug, serde::Serialize)]
21#[serde(rename_all = "snake_case")]
22pub enum Source {
23    Human,
24    Agent,
25}
26/// Implement this trait to build an app. UI construction is side-effect free.
27pub trait Application: 'static {
28    fn view(&self) -> Node;
29    fn update(&mut self, action: Action);
30    fn update_from(&mut self, action: Action, _source: Source) {
31        self.update(action);
32    }
33    fn theme(&self) -> Theme {
34        Theme::DARK
35    }
36    fn debug_bounds(&self) -> bool {
37        false
38    }
39    /// Optional domain state in automation responses. Never put secrets here.
40    fn inspect(&self) -> Value {
41        Value::Null
42    }
43}
44#[derive(Clone, Debug)]
45pub struct WindowOptions {
46    pub title: String,
47    pub width: f64,
48    pub height: f64,
49    pub agent: bool,
50}
51impl Default for WindowOptions {
52    fn default() -> Self {
53        Self {
54            title: "Forge UI".into(),
55            width: 1120.0,
56            height: 820.0,
57            agent: false,
58        }
59    }
60}
61#[derive(Debug, Deserialize)]
62#[serde(deny_unknown_fields)]
63struct Request {
64    #[serde(default)]
65    request_id: Option<String>,
66    op: String,
67    #[serde(default)]
68    id: String,
69    text: Option<String>,
70    value: Option<f32>,
71    key: Option<String>,
72    #[serde(default)]
73    shift: bool,
74    #[serde(default)]
75    command: bool,
76    path: Option<String>,
77}
78enum UserEvent {
79    Agent(Result<Request, String>),
80}
81struct Host<A: Application> {
82    app: A,
83    ui: Ui,
84    options: WindowOptions,
85    window: Option<Rc<Window>>,
86    surface: Option<Surface<Rc<Window>, Rc<Window>>>,
87    mods: ModifiersState,
88    error: Option<String>,
89}
90impl<A: Application> Host<A> {
91    fn rebuild(&mut self) -> Result<(), String> {
92        self.ui.theme = self.app.theme();
93        self.ui.debug_bounds = self.app.debug_bounds();
94        self.ui.set_root(self.app.view())
95    }
96    fn apply(&mut self, actions: Vec<Action>, source: Source) -> Result<(), String> {
97        for a in actions {
98            self.app.update_from(a, source);
99        }
100        self.rebuild()?;
101        if let Some(w) = &self.window {
102            w.request_redraw();
103        }
104        Ok(())
105    }
106    fn input(&mut self, input: Input) -> Result<(), String> {
107        let a = self.ui.event(input);
108        self.apply(a, Source::Human)
109    }
110    fn fail(&mut self, e: impl ToString, ev: &ActiveEventLoop) {
111        self.error = Some(e.to_string());
112        ev.exit();
113    }
114    fn draw(&mut self) -> Result<(), String> {
115        let Some(w) = &self.window else { return Ok(()) };
116        let size = w.inner_size();
117        if size.width == 0 || size.height == 0 {
118            return Ok(());
119        }
120        self.ui
121            .resize(size.width, size.height, w.scale_factor() as f32);
122        self.ui.paint();
123        let surface = self.surface.as_mut().ok_or("surface not initialized")?;
124        surface
125            .resize(
126                NonZeroU32::new(size.width).unwrap(),
127                NonZeroU32::new(size.height).unwrap(),
128            )
129            .map_err(|e| e.to_string())?;
130        let mut buffer = surface.buffer_mut().map_err(|e| e.to_string())?;
131        buffer.copy_from_slice(&self.ui.painter.pixels);
132        buffer.present().map_err(|e| e.to_string())
133    }
134    fn agent(&mut self, r: &Request, ev: &ActiveEventLoop) -> Result<Value, String> {
135        match r.op.as_str() {
136            "inspect" => {}
137            "click" | "set_text" | "set_value" => {
138                let actions = self
139                    .ui
140                    .agent_action(&r.op, &r.id, r.text.as_deref(), r.value)?;
141                self.apply(actions, Source::Agent)?;
142            }
143            "key" => {
144                let key = r.key.as_deref().ok_or("key is required")?;
145                if ![
146                    "Tab",
147                    "Enter",
148                    "Space",
149                    "ArrowLeft",
150                    "ArrowRight",
151                    "ArrowUp",
152                    "ArrowDown",
153                    "Home",
154                    "End",
155                    "Backspace",
156                    "Delete",
157                    "a",
158                    "A",
159                ]
160                .contains(&key)
161                {
162                    return Err("unsupported key".into());
163                }
164                let actions = self.ui.event(Input::Key {
165                    key: key.into(),
166                    shift: r.shift,
167                    command: r.command,
168                });
169                self.apply(actions, Source::Agent)?;
170            }
171            "screenshot" => {
172                self.draw()?;
173                let path = r.path.as_ref().ok_or("path is required")?;
174                self.ui
175                    .painter
176                    .save_ppm(std::path::Path::new(path))
177                    .map_err(|e| e.to_string())?;
178            }
179            "quit" => ev.exit(),
180            _ => return Err(format!("unknown operation: {}", r.op)),
181        }
182        // Complete the real rendering path before acknowledging agent mutations.
183        if r.op != "quit" {
184            self.draw()?;
185        }
186        Ok(json!({"tree":self.ui.inspect(),"app":self.app.inspect()}))
187    }
188}
189impl<A: Application> ApplicationHandler<UserEvent> for Host<A> {
190    fn resumed(&mut self, ev: &ActiveEventLoop) {
191        if self.window.is_some() {
192            return;
193        }
194        let result = (|| -> Result<(), String> {
195            let w = Rc::new(
196                ev.create_window(
197                    Window::default_attributes()
198                        .with_title(&self.options.title)
199                        .with_inner_size(LogicalSize::new(self.options.width, self.options.height))
200                        .with_min_inner_size(LogicalSize::new(760.0, 580.0)),
201                )
202                .map_err(|e| e.to_string())?,
203            );
204            let ctx = Context::new(w.clone()).map_err(|e| e.to_string())?;
205            let surface = Surface::new(&ctx, w.clone()).map_err(|e| e.to_string())?;
206            self.surface = Some(surface);
207            self.window = Some(w);
208            self.rebuild()?;
209            self.draw()?;
210            Ok(())
211        })();
212        if let Err(e) = result {
213            self.fail(e, ev);
214        } else if self.options.agent {
215            println!("{}", json!({"event":"ready","protocol":"forge-ui/1"}));
216        }
217    }
218    fn suspended(&mut self, _ev: &ActiveEventLoop) {
219        self.surface = None;
220        self.window = None;
221    }
222    fn user_event(&mut self, ev: &ActiveEventLoop, event: UserEvent) {
223        let UserEvent::Agent(request) = event;
224        match request {
225            Ok(r) => {
226                let result = self.agent(&r, ev);
227                let response = match result {
228                    Ok(v) => json!({"request_id":r.request_id,"ok":true,"result":v}),
229                    Err(e) => json!({"request_id":r.request_id,"ok":false,"error":e}),
230                };
231                println!("{response}");
232            }
233            Err(e) => println!("{}", json!({"ok":false,"error":e})),
234        }
235    }
236    fn window_event(&mut self, ev: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
237        let result = match event {
238            WindowEvent::CloseRequested => {
239                ev.exit();
240                Ok(())
241            }
242            WindowEvent::RedrawRequested => self.draw(),
243            WindowEvent::Resized(_) | WindowEvent::ScaleFactorChanged { .. } => {
244                if let Some(w) = &self.window {
245                    w.request_redraw();
246                }
247                Ok(())
248            }
249            WindowEvent::ModifiersChanged(m) => {
250                self.mods = m.state();
251                Ok(())
252            }
253            WindowEvent::Focused(false) => self.input(Input::Cancel),
254            WindowEvent::CursorLeft { .. } => self.input(Input::Move(-1.0, -1.0)),
255            WindowEvent::CursorMoved { position, .. } => {
256                let s = self
257                    .window
258                    .as_ref()
259                    .map(|w| w.scale_factor())
260                    .unwrap_or(1.0);
261                self.input(Input::Move(
262                    (position.x / s) as f32,
263                    (position.y / s) as f32,
264                ))
265            }
266            WindowEvent::MouseInput {
267                state,
268                button: MouseButton::Left,
269                ..
270            } => self.input(if state == ElementState::Pressed {
271                Input::Down
272            } else {
273                Input::Up
274            }),
275            WindowEvent::MouseWheel { delta, .. } => {
276                let s = self
277                    .window
278                    .as_ref()
279                    .map(|w| w.scale_factor())
280                    .unwrap_or(1.0);
281                self.input(Input::Wheel(match delta {
282                    MouseScrollDelta::LineDelta(_, y) => -y * 36.0,
283                    MouseScrollDelta::PixelDelta(p) => -p.y as f32 / s as f32,
284                }))
285            }
286            WindowEvent::KeyboardInput { event, .. } if event.state == ElementState::Pressed => {
287                let command = self.mods.control_key() || self.mods.super_key();
288                let key = match &event.logical_key {
289                    Key::Named(NamedKey::Space) => "Space".into(),
290                    Key::Named(k) => format!("{k:?}"),
291                    Key::Character(c) => c.to_string(),
292                    _ => String::new(),
293                };
294                let named = matches!(event.logical_key, Key::Named(_));
295                let r = self.input(Input::Key {
296                    key,
297                    shift: self.mods.shift_key(),
298                    command,
299                });
300                if r.is_ok() && !command {
301                    if let Some(text) = event.text {
302                        if !named || matches!(event.logical_key, Key::Named(NamedKey::Space)) {
303                            return if let Err(e) = self.input(Input::Text(text.to_string())) {
304                                self.fail(e, ev);
305                            };
306                        }
307                    }
308                }
309                r
310            }
311            _ => Ok(()),
312        };
313        if let Err(e) = result {
314            self.fail(e, ev);
315        }
316    }
317}
318/// Run a native window. Set `options.agent` to opt into local JSON-lines stdin/stdout.
319/// No server is opened. EOF leaves the window open; send `quit` to exit.
320pub fn run<A: Application>(app: A, options: WindowOptions) -> Result<(), Box<dyn Error>> {
321    let ui = Ui::new(app.view()).map_err(std::io::Error::other)?;
322    let event_loop = EventLoop::<UserEvent>::with_user_event().build()?;
323    event_loop.set_control_flow(ControlFlow::Wait);
324    if options.agent {
325        let proxy = event_loop.create_proxy();
326        std::thread::spawn(move || {
327            let stdin = std::io::stdin();
328            let mut input = stdin.lock();
329            loop {
330                let mut line = String::new();
331                let result = (&mut input).take(65537).read_line(&mut line);
332                let request = match result {
333                    Ok(0) => break,
334                    Ok(_) if line.len() > 65536 => Err("request exceeds 64 KiB".into()),
335                    Ok(_) => serde_json::from_str(&line).map_err(|e| e.to_string()),
336                    Err(e) => Err(e.to_string()),
337                };
338                let stop = line.len() > 65536;
339                if proxy.send_event(UserEvent::Agent(request)).is_err() || stop {
340                    break;
341                }
342            }
343        });
344    }
345    let mut host = Host {
346        app,
347        ui,
348        options,
349        window: None,
350        surface: None,
351        mods: ModifiersState::empty(),
352        error: None,
353    };
354    event_loop.run_app(&mut host)?;
355    if let Some(e) = host.error {
356        return Err(std::io::Error::other(e).into());
357    }
358    Ok(())
359}