sciter-sdk / rust-sciter

Rust bindings for Sciter
https://sciter.com
MIT License
806 stars 76 forks source link

How to embed into GameMaker window? #83

Open GirkovArpa opened 3 years ago

GirkovArpa commented 3 years ago

I'm on Windows 10 64bit.

I used sciter::Window::attach() to "attach" it to the GameMaker window.

Then called frame.load_html() and frame.run_app().

But the HTML isn't displayed. The GameMaker window freezes until I click the close button, then the game proceeds without the window being closed. It's as if the close button closes the attached (invisible) Sciter window the first time it's clicked.

If I use sciter::Window::create() instead, it works as expected, simply running the sciter application alongside the GameMaker game as 2 distinct programs.

But I want to embed the Sciter one as a transparent overlay.

pravic commented 3 years ago

What's GameMaker?

pravic commented 3 years ago

I believe the issue is with the difference in pumping messages. Do you attach from a different thread?

GirkovArpa commented 3 years ago

I'm referring to GameMaker Studio 2.

I will add that replacing frame.run_app() with frame.run_loop() makes no difference at all.

Also, using sciter::Window::new() instead of sciter::Window::attach() before the program's own window is displayed, causes the Sciter window to be displayed, and the GameMaker window doesn't appear until after the Sciter window is closed.

Using sciter::Window::new() a half-second after the GameMaker window appears causes the Sciter window to be displayed normally, as a separate program, but the separate GameMaker window display is frozen.

Clicking the close button on the GameMaker window causes it to unfreeze, but not close. Clicking close again closes both the GameMaker window and the Sciter window.

Closing the Sciter window will also close the GameMaker window.

I believe the issue is with the difference in pumping messages. Do you attach from a different thread?

GameMaker games are single-threaded, so it must be attaching from the main thread.

GirkovArpa commented 3 years ago

I can post a video if you want. Also if it matters, GameMaker uses DirectX11.

pravic commented 3 years ago

I will add that replacing frame.run_app() with frame.run_loop() makes no difference at all.

First, you don't need run_app() in your case - it runs its own message loop and the host application (GameMaker) will be frozen at that time. Or will behave like this:

Also, using sciter::Window::new() instead of sciter::Window::attach() before the program's own window is displayed, causes the Sciter window to be displayed, and the GameMaker window doesn't appear until after the Sciter window is closed.

Next, Window.attach is about attaching Sciter (as an HTML engine) to an existing native window.

Window.create creates a new native window and then attaches Sciter to it. So, the difference is about who creates a native window.

In any case, you have to route some WinAPI messages to Sciter in order to get UI working. And it can be done via SciterProcND - apparently, I haven't exposed it properly in Rust bindings, need to do.

So, you have to intercept messages in an existing window (for example, via SetWindowLongPtrW + GWLP_WNDPROC) and route messages to Sciter. Check out this article about some details.

Feel free to ask questions. Also, you can share your code or tell how you are trying to use that GameMaker, perhaps there is a better way.

GirkovArpa commented 3 years ago

I want to write a GameMaker extension that allows embedding a transparent Sciter UI into the game window. I'm just experimenting trying to get the Sciter window embedded.

I don't think GameMaker exposes the internal Windows event loop. Only the window handle.

I guess you mention GWLP_WNDPROC because according to the documentation it:

Sets a new address for the window procedure.

It's not immediately clear to me how this allows me to route window events to Sciter, but is it at least possible?

#[no_mangle]
pub extern "cdecl" fn foo(handle: *const c_char) -> f64 {
    let c_str: &CStr = unsafe { CStr::from_ptr(handle) };
    let hex_string: &str = c_str.to_str().unwrap();
    let window_handle = i64::from_str_radix(hex_string, 16).unwrap();
    println!("{:?}", window_handle);

    let handler = EventHandler {};
    //let mut frame = sciter::Window::new();
    let mut frame = sciter::Window::attach(window_handle as *mut sciter::types::_HWINDOW);
    frame.event_handler(handler);
    let dir = env::current_dir().unwrap().as_path().display().to_string();
    let filename = format!("{}\\{}", dir, "index.htm");
    println!("Full filename with path of index.htm: {}", filename);
    frame.load_html(include_bytes!("../index.htm"), None);
    frame.run_app();

    window_handle as f64
}
pravic commented 3 years ago

It's not immediately clear to me how this allows me to route window events to Sciter, but is it at least possible?

Something like this:

let hwnd = window_handle as sciter::types:HWINDOW;
hook_messages(hwnd);
let mut frame = sciter::Window::attach(hwnd);
fn hook_messages(hwnd: HWINDOW) {
    use sciter::types::*;

    #[link(name="user32")]
    extern "system"
    {
        fn SetWindowLongPtrW(hwnd: HWINDOW, index: i32, new_data: WndProc) -> WndProc;
        fn CallWindowProcW(prev: WndProc, hwnd: HWINDOW, msg: UINT, wp: WPARAM, lp: LPARAM) -> LRESULT;
    }

    type WndProc = extern "system" fn (hwnd: HWINDOW, msg: UINT, wp: WPARAM, lp: LPARAM) -> LRESULT;
    type PrevProcs = std::collections::HashMap<HWINDOW, WndProc>;

    thread_local! {
        static PREV_PROC: std::cell::RefCell<PrevProcs> = Default::default();
    }

    // https://sciter.com/developers/embedding-principles/
    extern "system" fn wnd_proc(hwnd: HWINDOW, msg: UINT, wp: WPARAM, lp: LPARAM) -> LRESULT {
        // first, pass the message to Sciter.
        let mut handled = false as BOOL;
        let lr = (sciter::SciterAPI().SciterProcND)(hwnd, msg, wp, lp, &mut handled);

        // if it was handled by Sciter, we're done here.
        if handled != 0 {
            return lr;
        }

        // if not, call the original window proc.
        let mut lr: LRESULT = 0;
        PREV_PROC.with(|procs| {
            let prev_proc = *procs.borrow().get(&hwnd).expect("An unregistered WindowProc is called somehow.");
            lr = unsafe { CallWindowProcW(prev_proc, hwnd, msg, wp, lp) }
        });

        // and return its result
        lr
    }

    // Subclass the window in order to receive its messages.
    const GWLP_WNDPROC: i32 = -4;
    let prev_proc = unsafe { SetWindowLongPtrW(hwnd, GWLP_WNDPROC, wnd_proc) };
    PREV_PROC.with(|procs| {
        procs.borrow_mut().insert(hwnd, prev_proc);
    });

}
GirkovArpa commented 3 years ago

I corrected a few typos but there's one error I'm not sure how to solve:

let lr = (_API.SciterProcND)(hwnd, msg, wp, lp, &mut handled);
          ^^^^ not found in this scope

Per the example here I tried writing this near the top of my file:

use ::{_API};

But I get this error:

use ::{_API};
        ^^^^ no `_API` external crate

This is the entire contents of lib.rs:

// $ cargo +stable-i686-pc-windows-gnu build --release

#![crate_type = "cdylib"]

extern crate libc;
use libc::c_char;
use std::ffi::CStr;
use std::i64;

extern crate sciter;
use std::env;

#[no_mangle]
pub extern "cdecl" fn foo(handle: *const c_char) -> f64 {
    let c_str: &CStr = unsafe { CStr::from_ptr(handle) };
    let hex_string: &str = c_str.to_str().unwrap();
    let window_handle = i64::from_str_radix(hex_string, 16).unwrap();
        println!("{:?}", window_handle);

        let hwnd = window_handle as sciter::types::HWINDOW;
        hook_messages(hwnd);
        let mut frame = sciter::Window::attach(hwnd);

    let dir = env::current_dir().unwrap().as_path().display().to_string();
    let filename = format!("{}\\{}", dir, "index.htm");
    println!("Full filename with path of index.htm: {}", filename);
    frame.load_html(include_bytes!("../index.htm"), None);
    frame.run_app();

    window_handle as f64
}

fn hook_messages(hwnd: sciter::types::HWINDOW) {
    use sciter::types::*;

    #[link(name="user32")]
    extern "system"
    {
        fn SetWindowLongPtrW(hwnd: HWINDOW, index: i32, new_data: WndProc) -> WndProc;
        fn CallWindowProcW(prev: WndProc, hwnd: HWINDOW, msg: UINT, wp: WPARAM, lp: LPARAM) -> LRESULT;
    }

    type WndProc = extern "system" fn (hwnd: HWINDOW, msg: UINT, wp: WPARAM, lp: LPARAM) -> LRESULT;
    type PrevProcs = std::collections::HashMap<HWINDOW, WndProc>;

    thread_local! {
        static PREV_PROC: std::cell::RefCell<PrevProcs> = Default::default();
    }

    // https://sciter.com/developers/embedding-principles/
    extern "system" fn wnd_proc(hwnd: HWINDOW, msg: UINT, wp: WPARAM, lp: LPARAM) -> LRESULT {
        // first, pass the message to Sciter.
        let mut handled = false as BOOL;
        let lr = (_API.SciterProcND)(hwnd, msg, wp, lp, &mut handled);

        // if it was handled by Sciter, we're done here.
        if handled != 0 {
            return lr;
        }

        // if not, call the original window proc.
        let mut lr: LRESULT = 0;
        PREV_PROC.with(|procs| {
            let prev_proc = *procs.borrow().get(&hwnd).expect("An unregistered WindowProc is called somehow.");
            lr = unsafe { CallWindowProcW(prev_proc, hwnd, msg, wp, lp) }
        });

        // and return its result
        lr
    }

    // Subclass the window in order to receive its messages.
    const GWLP_WNDPROC: i32 = -4;
    let prev_proc = unsafe { SetWindowLongPtrW(hwnd, GWLP_WNDPROC, wnd_proc) };
    PREV_PROC.with(|procs| {
        procs.borrow_mut().insert(hwnd, prev_proc);
    });
}

And cargo install _API says there's no crate with that name.

pravic commented 3 years ago

Sorry, it was a copy-paste from the implementation.

It should be sciter::SciterAPI().SciterProcND in your case.

GirkovArpa commented 3 years ago

It compiles with cargo build on 64bit Windows but not for 32bit Windows with cargo +stable-i686-pc-windows-gnu build because:

undefined reference to `SetWindowLongPtrW@12'

According to this issue in an unrelated repository, it's because:

I've just checked the winapi rs docs for the definition of Get/SetWindowLongPtrW and it is only exported for "x86_64" aka 64 bits Windows.

Unfortunately my version of GameMaker produces only 32bit executables*, so a 64bit .dll won't be compatible.

*Beta support for 64bit dropped earlier this month.

So this is really a problem with the winapi crate and GameMaker. If there's not an easy fix may as well close this.

pravic commented 3 years ago

You can use SetWindowLongW on 32-bit Windows. By the way, why do you use GCC instead of MSVC?

GirkovArpa commented 3 years ago

SetWindowLongW compiles. But (using frame.run_loop) the game window never even displays. It shows up in Task Manager as a process, but it's invisible.

Loading a .dll is blocking in GameMaker, so maybe that's why. But I put a println! statement inside wnd_proc and I can see it's constantly getting called, so not sure what's happening.

Loading the .dll after the game window is displayed just freezes it until the close button is clicked, upon which the game resumes.

There's some TIScript in index.htm with stdout.println calls, and they don't work either.


I used GCC because I prefer VSC over MSVC which on my computer takes forever to start and constantly freezes and lags.

pravic commented 3 years ago

You don't need calling run_loop. Hook the window, attach Sciter to it, load html and that's it. Ah yes, you need to show it: call window.expand(false).

In order to use MSVC you don't need VisualStudio, only build tools (compiler and sdk) : https://rust-lang.github.io/rustup/installation/windows.html

pravic commented 3 years ago

If you tell me how to use GameMaker and load a plugin in it, step by step, I'll try to check what's wrong with Sciter.

GirkovArpa commented 3 years ago

I tried adding frame.expand(false) with no success. I published a repository of what I'm doing here if you want to look at it.

You can see the entirety of the GameMaker script in the README:

var foo = external_define(
    "sciter_gamemaker.dll", 
    "foo", 
    dll_cdecl,
    ty_real, 
    1, 
    ty_string
);
var handle = window_handle();
var handle_as_hex_string = string(handle);
external_call(foo, handle_as_hex_string);

Is there an advantage to using MSVC over GCC?

GirkovArpa commented 3 years ago

In order to use MSVC you don't need VisualStudio, only build tools (compiler and sdk) : https://rust-lang.github.io/rustup/installation/windows.html

I actually replied before clicking your link. I thought you were referring to my other repositories where I used GCC. I didn't realize that Rust was using GCC to compile; I thought it had its own compiler.

pravic commented 3 years ago

Rust has its own compiler, but uses an external linker and libraries (either GCC or MSVC). Anyway, it's not relevant to Game Maker. I'll try to check.

GirkovArpa commented 2 years ago

Any working examples of embedding into any window using sciter-rs?

I can't get this to work even with a Notepad window.

SkyLeite commented 1 month ago

I'm running into the same issue. No matter what I do the HTML is simply not rendered.