q 0.1.1

Q is a Rust GDExtension crate for Godot, offering a Game Manager, Music Manager, Bevy ECS, and Tokio-powered multi-threading. It simplifies and enhances game development with efficient tools and scalable performance.
Documentation
use godot::classes::{Control, IControl, Os, ProjectSettings};
use godot::prelude::*;

#[cfg(target_os = "linux")]
use crate::platform::linux::LinuxWryBrowserOptions;
#[cfg(target_os = "macos")]
use crate::platform::macos::MacOSWryBrowserOptions;
#[cfg(target_os = "windows")]
use crate::platform::windows::WindowsWryBrowserOptions;

#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
use wry::{
    Rect, WebViewBuilder,
    dpi::{PhysicalPosition, PhysicalSize},
    http::Request,
};

#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
use std::{borrow::Cow, fs, path::PathBuf};

#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
use http::{Response, header::CONTENT_TYPE};

#[derive(GodotClass)]
#[class(base = Control)]
pub struct GodotBrowser {
    base: Base<Control>,

    #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
    webview: Option<wry::WebView>,

    full_window_size: bool,
    url: GString,
    html: GString,
    transparent: bool,
    background_color: Color,
    devtools: bool,
    user_agent: GString,
    zoom_hotkeys: bool,
    clipboard: bool,
    incognito: bool,
    focused: bool,
}

#[godot_api]
impl IControl for GodotBrowser {
    fn init(base: Base<Control>) -> Self {
        Self {
            base,

            #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
            webview: None,

            full_window_size: true,
            url: "https://kbve.com/".into(),
            html: "".into(),
            transparent: false,
            background_color: Color::from_rgb(1.0, 1.0, 1.0),
            devtools: true,
            user_agent: "".into(),
            zoom_hotkeys: false,
            clipboard: true,
            incognito: false,
            focused: true,
        }
    }

    fn ready(&mut self) {
        #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
        {
            #[cfg(target_os = "macos")]
            let window = MacOSWryBrowserOptions;

            #[cfg(target_os = "windows")]
            let window = WindowsWryBrowserOptions;

            #[cfg(target_os = "linux")]
            let window = LinuxWryBrowserOptions;

            let base = self.base().clone();
            let mut builder = WebViewBuilder::new()
                .with_transparent(self.transparent)
                .with_devtools(self.devtools)
                .with_hotkeys_zoom(self.zoom_hotkeys)
                .with_clipboard(self.clipboard)
                .with_incognito(self.incognito)
                .with_focused(self.focused)
                .with_custom_protocol("res".into(), |_id, req| get_res_response(req))
                .with_ipc_handler(move |req: Request<String>| {
                    let body = req.body().as_str();
                    base.clone()
                        .emit_signal("ipc_message", &[body.to_variant()]);
                });

            if !self.user_agent.is_empty() {
                builder = builder.with_user_agent(self.user_agent.to_string());
            }

            if self.html.is_empty() && !self.url.is_empty() {
                builder = builder.with_url(self.url.to_string());
            } else if !self.html.is_empty() && self.url.is_empty() {
                builder = builder.with_html(self.html.to_string());
            }

            let webview_builder = builder;

            if !self.url.is_empty() && !self.html.is_empty() {
                godot_error!(
                    "[GodotBrowser] You have entered both a URL and HTML code. Only one can be used."
                );
                return;
            }

            match webview_builder.build_as_child(&window) {
                Ok(webview) => {
                    self.webview.replace(webview);
                    self.resize();
                }
                Err(e) => {
                    godot_error!("[GodotBrowser] Failed to create WebView: {:?}", e);
                }
            }
        }
    }
}

#[godot_api]
impl GodotBrowser {
    #[signal]
    fn ipc_message(message: GString);

    #[func]
    pub fn is_initialized(&self) -> bool {
        self.webview.is_some()
    }

    #[func]
    fn post_message(&self, message: GString) {
        if let Some(webview) = &self.webview {
            let escaped_message = message.to_string().replace("'", "\\'");
            let script = format!(
                "document.dispatchEvent(new CustomEvent('message', {{ detail: '{}' }}))",
                escaped_message
            );
            let _ = webview.evaluate_script(&script);
        }
    }

    #[func]
    pub fn open_url(&self, url: GString) {
        #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
        if let Some(webview) = &self.webview {
            let url_str = url.to_string();
            let script = format!("window.location.href = '{}';", url_str.replace('\'', "\\'"));
            if let Err(e) = webview.evaluate_script(&script) {
                godot_error!("[GodotBrowser] Failed to navigate to {}: {:?}", url_str, e);
            }
        }
    }

    #[func]
    pub fn resize(&self) {
        if let Some(webview) = &self.webview {
            let rect = {
                let viewport_size = self
                    .base()
                    .get_tree()
                    .and_then(|tree| tree.get_root())
                    .map(|viewport| viewport.get_size())
                    .unwrap_or(Vector2i::new(800, 600));

                Rect {
                    position: PhysicalPosition::new(0, 0).into(),
                    size: PhysicalSize::new(viewport_size.x as u32, viewport_size.y as u32).into(),
                }
            };
            if let Err(e) = webview.set_bounds(rect) {
                godot_error!("[GodotBrowser] Failed to resize WebView: {:?}", e);
            } else {
                godot_print!("[GodotBrowser] WebView resized to {:?}.", rect.size);
            }
        }
    }
}

#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
pub fn get_res_response(request: Request<Vec<u8>>) -> Response<Cow<'static, [u8]>> {
    let os = Os::singleton();
    let root = if os.has_feature("editor") {
        let project_settings = ProjectSettings::singleton();
        PathBuf::from(String::from(project_settings.globalize_path("res://")))
    } else {
        let mut dir = PathBuf::from(String::from(os.get_executable_path()));
        dir.pop();
        dir
    };

    let path = format!(
        "{}{}",
        request.uri().host().unwrap_or_default(),
        request.uri().path()
    );
    let full_path = root.join(path);
    if full_path.exists() && full_path.is_file() {
        let content = fs::read(full_path).expect("Failed to read file");
        let mime_str = infer::get(&content)
            .map(|m| m.to_string())
            .unwrap_or_else(|| "application/octet-stream".to_string());
        return http::Response::builder()
            .header(CONTENT_TYPE, mime_str)
            .status(200)
            .body(content)
            .unwrap()
            .map(Into::into);
    }

    http::Response::builder()
        .header(CONTENT_TYPE, "text/plain")
        .status(404)
        .body(
            format!("Could not find file at {:?}", full_path)
                .as_bytes()
                .to_vec(),
        )
        .unwrap()
        .map(Into::into)
}