minimal Qt6 UI

This commit is contained in:
numzero 2025-11-15 01:27:07 +03:00
parent 10d74f1318
commit 4e4c4493f9
14 changed files with 337 additions and 3 deletions

View File

@ -5,7 +5,10 @@ project(photon_light VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS Gui Widgets)
find_program(CARGO cargo REQUIRED) find_program(CARGO cargo REQUIRED)
set(CARGO_TARGET_DIR "${CMAKE_BINARY_DIR}/cargo") set(CARGO_TARGET_DIR "${CMAKE_BINARY_DIR}/cargo")
qt_standard_project_setup()
add_subdirectory(ui) add_subdirectory(ui)

4
Cargo.lock generated
View File

@ -1169,7 +1169,11 @@ dependencies = [
name = "photon-light-impl" name = "photon-light-impl"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"glam",
"photon-light", "photon-light",
"pollster",
"raw-window-handle",
"wgpu",
] ]
[[package]] [[package]]

View File

@ -207,6 +207,7 @@ impl Core {
self.queue.submit(std::iter::once(encoder.finish())); self.queue.submit(std::iter::once(encoder.finish()));
} }
/// Configures the renderer for a given target size.
pub fn configure(&mut self, pixel_size: UVec2) { pub fn configure(&mut self, pixel_size: UVec2) {
self.surface.configure( self.surface.configure(
&self.device, &self.device,
@ -223,6 +224,9 @@ impl Core {
); );
} }
/// Redraws the entire surface.
///
/// [`Self::configure`] must be called at least once before this.
pub fn redraw(&mut self) { pub fn redraw(&mut self) {
let output = self.surface.get_current_texture().unwrap(); let output = self.surface.get_current_texture().unwrap();
self.render(&output.texture); self.render(&output.texture);

View File

@ -1,7 +1,13 @@
include(impl.cmake) include(impl.cmake)
add_executable(photon_light qt_add_executable(photon_light
src/api.cxx
src/main.cxx src/main.cxx
src/main_window.cxx
src/main_window.ui
src/viewport.cxx
) )
target_link_libraries(photon_light PRIVATE Qt6::Gui Qt6::Widgets)
target_link_libraries(photon_light PRIVATE photon_light_impl) target_link_libraries(photon_light PRIVATE photon_light_impl)
target_include_directories(photon_light PRIVATE src)

View File

@ -8,3 +8,8 @@ crate-type = ["staticlib"]
[dependencies] [dependencies]
photon-light = {path = "../"} photon-light = {path = "../"}
glam = { version = "0.30" }
pollster = "0.4.0"
raw-window-handle = "0.6.2"
wgpu = "27.0.1"

56
ui/src/api.cxx Normal file
View File

@ -0,0 +1,56 @@
#include "api.hxx"
#include <utility>
#include <stdexcept>
extern "C" Core* rt4_viewport_create(xcb_connection_t* connection, std::uint32_t window);
extern "C" void rt4_viewport_destroy(Core* viewport);
extern "C" void rt4_viewport_configure(Core* viewport, std::uint32_t width, std::uint32_t height);
extern "C" void rt4_viewport_redraw(Core* viewport);
BoxCore::BoxCore(BoxCore&& b)
: ptr(std::exchange(b.ptr, nullptr))
{
}
BoxCore::~BoxCore() {
reset();
}
BoxCore& BoxCore::operator= (BoxCore&& b) {
if (&b == this)
return *this;
std::swap(ptr, b.ptr);
b.reset();
return *this;
}
BoxCore BoxCore::from_xcb(xcb_connection_t* connection, std::uint32_t window) {
if (!connection)
throw std::logic_error("attempt to use a null connection");
if (!window)
throw std::logic_error("attempt to use a null window");
BoxCore out;
out.ptr = rt4_viewport_create(connection, window);
return out;
}
void BoxCore::reset() {
auto viewport = std::exchange(ptr, nullptr);
if (viewport)
rt4_viewport_destroy(viewport);
}
Core* BoxCore::use() const {
if (!ptr)
throw std::logic_error("attempt to use a null Core");
return ptr;
}
void BoxCore::configure(std::uint32_t width, std::uint32_t height) {
rt4_viewport_configure(use(), width, height);
}
void BoxCore::redraw() {
rt4_viewport_redraw(use());
}

30
ui/src/api.hxx Normal file
View File

@ -0,0 +1,30 @@
#pragma once
#include <cstdint>
struct xcb_connection_t;
struct Core;
class BoxCore {
public:
BoxCore() = default;
BoxCore(const BoxCore&) = delete;
BoxCore(BoxCore&&);
BoxCore& operator= (const BoxCore&) = delete;
BoxCore& operator= (BoxCore&&);
~BoxCore();
explicit operator bool() const { return ptr; }
void reset();
static BoxCore from_xcb(xcb_connection_t* connection, std::uint32_t window);
void configure(std::uint32_t width, std::uint32_t height);
void redraw();
private:
Core* ptr = nullptr;
Core* use() const;
};

View File

@ -0,0 +1,45 @@
use std::{ffi::c_void, num::NonZero, ptr::NonNull};
use glam::uvec2;
use photon_light::{Core, init_gpu_inner};
use raw_window_handle::{RawDisplayHandle, RawWindowHandle, XcbDisplayHandle, XcbWindowHandle};
unsafe fn create_viewport(
display: impl Into<RawDisplayHandle>,
window: impl Into<RawWindowHandle>,
) -> Box<Core> {
let target = wgpu::SurfaceTargetUnsafe::RawHandle {
raw_display_handle: display.into(),
raw_window_handle: window.into(),
};
let gpu = pollster::block_on(init_gpu_inner(|instance| unsafe {
instance.create_surface_unsafe(target)
}))
.unwrap();
Box::new(Core::new(gpu))
}
#[unsafe(no_mangle)]
unsafe extern "C" fn rt4_viewport_create(
connection: NonNull<c_void>,
window: NonZero<u32>,
) -> Box<Core> {
let display = XcbDisplayHandle::new(Some(connection), 0);
let window = XcbWindowHandle::new(window);
unsafe { create_viewport(display, window) }
}
#[unsafe(no_mangle)]
unsafe extern "C" fn rt4_viewport_destroy(viewport: Box<Core>) {
drop(viewport);
}
#[unsafe(no_mangle)]
unsafe extern "C" fn rt4_viewport_configure(viewport: &mut Core, width: u32, height: u32) {
viewport.configure(uvec2(width, height));
}
#[unsafe(no_mangle)]
unsafe extern "C" fn rt4_viewport_redraw(viewport: &mut Core) {
viewport.redraw();
}

View File

@ -1,3 +1,13 @@
int main() { #include "main_window.hxx"
return 0;
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
auto w = new PhotonLight;
w->show();
return app.exec();
} }

14
ui/src/main_window.cxx Normal file
View File

@ -0,0 +1,14 @@
#include "main_window.hxx"
#include "ui_main_window.h"
PhotonLight::PhotonLight(QWidget *parent)
: QMainWindow(parent)
, m_ui(new Ui::MainWindow)
{
m_ui->setupUi(this);
}
PhotonLight::~PhotonLight() = default;
#include "moc_main_window.cpp"

20
ui/src/main_window.hxx Normal file
View File

@ -0,0 +1,20 @@
#pragma once
#include <QMainWindow>
#include <memory>
namespace Ui {
class MainWindow;
}
class PhotonLight : public QMainWindow
{
Q_OBJECT
public:
explicit PhotonLight(QWidget *parent = nullptr);
~PhotonLight() override;
private:
const std::unique_ptr<Ui::MainWindow> m_ui;
};

45
ui/src/main_window.ui Normal file
View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1220</width>
<height>823</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="Viewport" name="widget" native="true"/>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1220</width>
<height>38</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<customwidgets>
<customwidget>
<class>Viewport</class>
<extends>QWidget</extends>
<header>viewport.hxx</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

67
ui/src/viewport.cxx Normal file
View File

@ -0,0 +1,67 @@
#include "viewport.hxx"
#include <QApplication>
#include <QEvent>
#include <QGuiApplication>
Viewport::Viewport(QWidget* parent, Qt::WindowFlags f) : QWidget(parent, f) {
setAttribute(Qt::WA_NativeWindow);
setAttribute(Qt::WA_PaintOnScreen);
setAttribute(Qt::WA_NoSystemBackground);
}
Viewport::~Viewport() = default;
QPaintEngine* Viewport::paintEngine() const {
return nullptr;
}
bool Viewport::event(QEvent* event) {
switch (event->type()) {
case QEvent::Type::WinIdChange:
recreate();
break;
default:
break;
}
return QWidget::event(event);
}
void Viewport::paintEvent(QPaintEvent* event) {
if (!core)
recreate();
core.redraw();
}
void Viewport::resizeEvent(QResizeEvent* event) {
if (!core)
return;
updateSize();
QWidget::resizeEvent(event);
}
void Viewport::recreate() try {
auto* app = qobject_cast<QGuiApplication*>(QApplication::instance());
if (!app)
throw std::runtime_error("not a GUI application (WTF?)");
auto* native = app->nativeInterface<QNativeInterface::QX11Application>();
if (!native)
throw std::runtime_error("X11 interface is not available");
auto* xcb_connection = native->connection();
std::uint32_t x11_window = winId();
fprintf(stderr, "connection %p, window %#08x\n", xcb_connection, x11_window);
core.reset();
core = BoxCore::from_xcb(xcb_connection, x11_window);
updateSize();
} catch (const std::exception& e) {
fprintf(stderr, "failed to recreate the viewport: %s", e.what());
}
void Viewport::updateSize() {
const QSize device_size = size() * devicePixelRatio();
core.configure(device_size.width(), device_size.height());
}

25
ui/src/viewport.hxx Normal file
View File

@ -0,0 +1,25 @@
#pragma once
#include <QWidget>
#include "api.hxx"
class Viewport: public QWidget {
Q_OBJECT
public:
explicit Viewport(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());
~Viewport() override;
QPaintEngine* paintEngine() const override;
protected:
bool event(QEvent* event) override;
void paintEvent(QPaintEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
private:
BoxCore core;
void recreate();
void updateSize();
};