From 364d8d3ae235e9e172929a11609bb2978b4385cb Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Mon, 29 Jun 2026 21:50:53 +0800 Subject: [PATCH 1/4] feat(platform): add embedded DirectRender backend for linuxfb/EGLFS Add desktop/ui/platform/embedded/ providing an IDisplayServerBackend implementation for the DirectRender role, used when CFDesktop renders directly to the framebuffer (Qt linuxfb/EGLFS) without a windowing system -- e.g. i.MX6ULL. - EmbeddedDisplayServerBackend: DirectRender role, primary-screen output, no-op window backend. - NullWindowBackend: empty IWindowBackend; reports no multi-window/GPU. - EmbeddedDisplaySizePolicy: frameless fullscreen for one framebuffer. - embedded_platform.cpp: native_impl / native_display_impl / native_shell_layer_impl (shell layer reuses WidgetShellLayer). - CMake: CFDESKTOP_EMBEDDED option selects embedded/ instead of linux_wsl/ (XCB) or windows/, so the ARM build needs no libxcb. Enables cross-compiling for armhf (arm-none-linux-gnueabihf, GCC 15.2, Qt 6.9.1 linuxfb) with -DCFDESKTOP_EMBEDDED=ON. --- desktop/ui/platform/CMakeLists.txt | 23 ++- .../embedded_display_server_backend.cpp | 71 +++++++++ .../embedded_display_server_backend.h | 150 ++++++++++++++++++ .../embedded/embedded_display_size_policy.cpp | 41 +++++ .../embedded/embedded_display_size_policy.h | 92 +++++++++++ .../platform/embedded/embedded_platform.cpp | 76 +++++++++ .../platform/embedded/null_window_backend.cpp | 42 +++++ .../platform/embedded/null_window_backend.h | 101 ++++++++++++ 8 files changed, 592 insertions(+), 4 deletions(-) create mode 100644 desktop/ui/platform/embedded/embedded_display_server_backend.cpp create mode 100644 desktop/ui/platform/embedded/embedded_display_server_backend.h create mode 100644 desktop/ui/platform/embedded/embedded_display_size_policy.cpp create mode 100644 desktop/ui/platform/embedded/embedded_display_size_policy.h create mode 100644 desktop/ui/platform/embedded/embedded_platform.cpp create mode 100644 desktop/ui/platform/embedded/null_window_backend.cpp create mode 100644 desktop/ui/platform/embedded/null_window_backend.h diff --git a/desktop/ui/platform/CMakeLists.txt b/desktop/ui/platform/CMakeLists.txt index f3cf0302c..415c53b7f 100644 --- a/desktop/ui/platform/CMakeLists.txt +++ b/desktop/ui/platform/CMakeLists.txt @@ -19,8 +19,17 @@ set(PLATFORM_HEADERS shell_layer_helper.h ) -# Add platform-specific subdirectories -if(UNIX AND NOT APPLE) +# Add platform-specific subdirectories. +# CFDESKTOP_EMBEDDED selects the DirectRender backend (Qt linuxfb / EGLFS) for +# targets without a windowing system (e.g. i.MX6ULL). It is mutually exclusive +# with the linux_wsl X11 backend and the Windows backend. +option(CFDESKTOP_EMBEDDED "Build the embedded DirectRender platform backend (linuxfb/EGLFS, no X11/compositor)" OFF) + +if(CFDESKTOP_EMBEDDED) + # Embedded DirectRender platform -- no X11, no compositor. + file(GLOB embedded_sources "${CMAKE_CURRENT_SOURCE_DIR}/embedded/*.cpp") + list(APPEND PLATFORM_SOURCES ${embedded_sources}) +elseif(UNIX AND NOT APPLE) # Linux/WSL platform if(IS_WSL OR EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/linux_wsl") file(GLOB linux_wsl_sources "${CMAKE_CURRENT_SOURCE_DIR}/linux_wsl/*.cpp") @@ -54,7 +63,11 @@ target_include_directories(cf_desktop_ui_platform PUBLIC ) # Add platform-specific subdirectory includes -if(UNIX AND NOT APPLE) +if(CFDESKTOP_EMBEDDED) + target_include_directories(cf_desktop_ui_platform PUBLIC + $ + ) +elseif(UNIX AND NOT APPLE) if(IS_WSL OR EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/linux_wsl") target_include_directories(cf_desktop_ui_platform PUBLIC $ @@ -81,7 +94,9 @@ PRIVATE QuarkWidgets::quarkwidgets # For qt_format.h (std::formatter) ) -if(UNIX AND NOT APPLE) +if(CFDESKTOP_EMBEDDED) + log_info("UI Backend" "Using Embedded DirectRender Backend (linuxfb/EGLFS).") +elseif(UNIX AND NOT APPLE) if(XCB_FOUND) log_info("UI Backend" "Using XCB Backend as the Desktop UI Render Backend.") target_link_libraries(cf_desktop_ui_platform PRIVATE PkgConfig::XCB) diff --git a/desktop/ui/platform/embedded/embedded_display_server_backend.cpp b/desktop/ui/platform/embedded/embedded_display_server_backend.cpp new file mode 100644 index 000000000..2a868f929 --- /dev/null +++ b/desktop/ui/platform/embedded/embedded_display_server_backend.cpp @@ -0,0 +1,71 @@ +/** + * @file embedded_display_server_backend.cpp + * @brief DirectRender IDisplayServerBackend implementation for embedded targets. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-29 + * @version 0.1 + * @since 0.19.0 + * @ingroup platform_embedded + */ + +#include "embedded_display_server_backend.h" + +#include "cflog.h" + +#include +#include + +namespace cf::desktop::backend::embedded { + +EmbeddedDisplayServerBackend::EmbeddedDisplayServerBackend(QObject* parent) + : IDisplayServerBackend(parent), + window_backend_(std::make_unique(this)) {} + +EmbeddedDisplayServerBackend::~EmbeddedDisplayServerBackend() = default; + +DisplayServerRole EmbeddedDisplayServerBackend::role() const { + return DisplayServerRole::DirectRender; +} + +DisplayServerCapabilities EmbeddedDisplayServerBackend::capabilities() const { + DisplayServerCapabilities caps; + caps.role = DisplayServerRole::DirectRender; + caps.canManageExternalWindows = false; + caps.needsOwnCompositor = false; + caps.supportsWaylandProtocol = false; + caps.supportsX11Protocol = false; + return caps; +} + +bool EmbeddedDisplayServerBackend::initialize(int /*argc*/, char** /*argv*/) { + initialized_ = true; + cf::log::traceftag("EmbeddedBackend", "Initialized - DirectRender (linuxfb/EGLFS)"); + return true; +} + +void EmbeddedDisplayServerBackend::shutdown() { + initialized_ = false; +} + +int EmbeddedDisplayServerBackend::runEventLoop() { + // The Qt event loop is driven by the desktop session (QApplication::exec()). + return 0; +} + +aex::WeakPtr EmbeddedDisplayServerBackend::windowBackend() { + if (!window_backend_) { + return {}; + } + return window_backend_->make_weak(); +} + +QList EmbeddedDisplayServerBackend::outputs() const { + QList result; + if (auto* screen = QGuiApplication::primaryScreen()) { + result.append(screen->geometry()); + } + return result; +} + +} // namespace cf::desktop::backend::embedded diff --git a/desktop/ui/platform/embedded/embedded_display_server_backend.h b/desktop/ui/platform/embedded/embedded_display_server_backend.h new file mode 100644 index 000000000..911e0e679 --- /dev/null +++ b/desktop/ui/platform/embedded/embedded_display_server_backend.h @@ -0,0 +1,150 @@ +/** + * @file embedded_display_server_backend.h + * @brief DirectRender IDisplayServerBackend for embedded framebuffer targets. + * + * EmbeddedDisplayServerBackend is the display server backend used when + * CFDesktop renders directly to the framebuffer (Qt linuxfb / EGLFS) without + * a windowing system. It reports the DirectRender role, exposes the primary + * screen as the single output, and provides a NullWindowBackend so the shell + * boots standalone. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-29 + * @version 0.1 + * @since 0.19.0 + * @ingroup platform_embedded + */ + +#pragma once + +#include "../../components/IDisplayServerBackend.h" +#include "null_window_backend.h" + +#include + +namespace cf::desktop::backend::embedded { + +/** + * @brief DirectRender display server backend for embedded framebuffer targets. + * + * Operates without an external window manager: the Qt platform plugin + * (linuxfb/EGLFS) owns the framebuffer, and this backend only reports the + * primary output geometry and a no-op window backend. + * + * @ingroup platform_embedded + */ +class EmbeddedDisplayServerBackend : public IDisplayServerBackend { + Q_OBJECT + public: + /** + * @brief Constructs the embedded DirectRender backend. + * @param[in] parent Optional Qt parent. + * @throws None + * @note None + * @warning None + * @since 0.19.0 + * @ingroup platform_embedded + */ + explicit EmbeddedDisplayServerBackend(QObject* parent = nullptr); + + /** + * @brief Destroys the embedded DirectRender backend. + * @throws None + * @note None + * @warning None + * @since 0.19.0 + * @ingroup platform_embedded + */ + ~EmbeddedDisplayServerBackend() override; + + /** + * @brief Returns the DirectRender role. + * @return DisplayServerRole::DirectRender. + * @throws None + * @note None + * @warning None + * @since 0.19.0 + * @ingroup platform_embedded + */ + DisplayServerRole role() const override; + + /** + * @brief Returns capabilities for a standalone framebuffer target. + * @return DisplayServerCapabilities with no external-window or + * protocol support. + * @throws None + * @note None + * @warning None + * @since 0.19.0 + * @ingroup platform_embedded + */ + DisplayServerCapabilities capabilities() const override; + + /** + * @brief Initializes the backend (the QPA plugin owns the framebuffer). + * @param[in] argc Argument count from the application entry point (unused). + * @param[in] argv Argument vector from the application entry point (unused). + * @return True unconditionally; initialization cannot fail. + * @throws None + * @note The Qt platform plugin (linuxfb/EGLFS) is selected via + * QT_QPA_PLATFORM before the QApplication is created. + * @warning None + * @since 0.19.0 + * @ingroup platform_embedded + */ + bool initialize(int argc, char** argv) override; + + /** + * @brief Releases resources held by the backend. + * @throws None + * @note None + * @warning None + * @since 0.19.0 + * @ingroup platform_embedded + */ + void shutdown() override; + + /** + * @brief Runs the backend event loop. + * @return Always zero; the Qt event loop is driven by the desktop + * session via QApplication::exec(). + * @throws None + * @note Not invoked on the Linux boot path. + * @warning None + * @since 0.19.0 + * @ingroup platform_embedded + */ + int runEventLoop() override; + + /** + * @brief Returns the no-op window backend. + * @return Weak pointer to the NullWindowBackend, or empty before + * initialization. + * @throws None + * @note None + * @warning None + * @since 0.19.0 + * @ingroup platform_embedded + */ + aex::WeakPtr windowBackend() override; + + /** + * @brief Returns the primary screen geometry as the single output. + * @return List containing the primary screen rectangle, or empty when + * no screen is available yet. + * @throws None + * @note None + * @warning None + * @since 0.19.0 + * @ingroup platform_embedded + */ + QList outputs() const override; + + private: + /// No-op window backend. Ownership: this. + std::unique_ptr window_backend_; + /// Whether initialize() has succeeded. + bool initialized_{false}; +}; + +} // namespace cf::desktop::backend::embedded diff --git a/desktop/ui/platform/embedded/embedded_display_size_policy.cpp b/desktop/ui/platform/embedded/embedded_display_size_policy.cpp new file mode 100644 index 000000000..3a92ebca2 --- /dev/null +++ b/desktop/ui/platform/embedded/embedded_display_size_policy.cpp @@ -0,0 +1,41 @@ +/** + * @file embedded_display_size_policy.cpp + * @brief Embedded framebuffer display size policy implementation. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-29 + * @version 0.1 + * @since 0.19.0 + * @ingroup platform_embedded + */ + +#include "embedded_display_size_policy.h" + +#include +#include + +namespace cf::desktop::platform_strategy::embedded { + +EmbeddedDisplaySizePolicy::EmbeddedDisplaySizePolicy() = default; + +EmbeddedDisplaySizePolicy::~EmbeddedDisplaySizePolicy() = default; + +const char* EmbeddedDisplaySizePolicy::name() const noexcept { + return "Embedded linuxfb Size Policy"; +} + +bool EmbeddedDisplaySizePolicy::action(QWidget* widget_data) { + if (widget_data == nullptr) { + return false; + } + // Embedded targets render directly to the framebuffer; drop any window + // decorations that a compositor would otherwise provide. + widget_data->setWindowFlag(Qt::FramelessWindowHint, true); + return true; +} + +DesktopBehaviors EmbeddedDisplaySizePolicy::query() const { + return DesktopBehaviorFlag::Fullscreen | DesktopBehaviorFlag::Frameless; +} + +} // namespace cf::desktop::platform_strategy::embedded diff --git a/desktop/ui/platform/embedded/embedded_display_size_policy.h b/desktop/ui/platform/embedded/embedded_display_size_policy.h new file mode 100644 index 000000000..0b4db7794 --- /dev/null +++ b/desktop/ui/platform/embedded/embedded_display_size_policy.h @@ -0,0 +1,92 @@ +/** + * @file embedded_display_size_policy.h + * @brief Embedded (linuxfb / EGLFS) display size policy strategy. + * + * Provides a minimal display size strategy for embedded targets that render + * directly to the framebuffer without a window manager. Unlike the WSL X11 + * policy, it does not probe for X11/Wayland and simply requests a frameless + * fullscreen surface. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-29 + * @version 0.1 + * @since 0.19.0 + * @ingroup platform_embedded + */ + +#pragma once + +#include "../IDesktopDisplaySizeStrategy.h" + +class QWidget; + +namespace cf::desktop::platform_strategy::embedded { + +/** + * @brief Display size policy for embedded framebuffer targets. + * + * Configures the desktop widget for frameless fullscreen rendering on a + * single framebuffer output (Qt linuxfb / EGLFS). No window-manager + * interaction is performed. + * + * @ingroup platform_embedded + */ +class EmbeddedDisplaySizePolicy : public IDesktopDisplaySizeStrategy { + public: + /** + * @brief Constructs the embedded display size policy. + * @throws None + * @note None + * @warning None + * @since 0.19.0 + * @ingroup platform_embedded + */ + EmbeddedDisplaySizePolicy(); + + /** + * @brief Destroys the embedded display size policy. + * @throws None + * @note None + * @warning None + * @since 0.19.0 + * @ingroup platform_embedded + */ + ~EmbeddedDisplaySizePolicy() override; + + /** + * @brief Returns the ABI-friendly name of this strategy. + * @return Null-terminated static string identifier. + * @throws None + * @note The returned string must not be freed. + * @warning None + * @since 0.19.0 + * @ingroup platform_embedded + */ + const char* name() const noexcept override; + + /** + * @brief Applies frameless fullscreen configuration to the widget. + * @param[in] widget_data Pointer to the QWidget to configure. May be nullptr. + * @return True when the widget was configured, false on nullptr. + * @throws None + * @note The widget is shown fullscreen by the desktop entity; + * this call only clears window decorations. + * @warning Passing nullptr results in an immediate false return. + * @since 0.19.0 + * @ingroup platform_embedded + */ + bool action(QWidget* widget_data) override; + + /** + * @brief Queries the desktop behaviors supported by this strategy. + * @return DesktopBehaviors with Fullscreen and Frameless flags set. + * @throws None + * @note None + * @warning None + * @since 0.19.0 + * @ingroup platform_embedded + */ + DesktopBehaviors query() const override; +}; + +} // namespace cf::desktop::platform_strategy::embedded diff --git a/desktop/ui/platform/embedded/embedded_platform.cpp b/desktop/ui/platform/embedded/embedded_platform.cpp new file mode 100644 index 000000000..31150dafa --- /dev/null +++ b/desktop/ui/platform/embedded/embedded_platform.cpp @@ -0,0 +1,76 @@ +/** + * @file embedded_platform.cpp + * @brief Native platform factory hooks for the embedded DirectRender backend. + * + * Provides the per-platform factory entry points (native_impl, + * native_display_impl, native_shell_layer_impl) consumed by the platform + * helpers when CFDESKTOP_EMBEDDED is enabled. The display backend is the + * EmbeddedDisplayServerBackend; the shell layer reuses the QWidget-based + * WidgetShellLayer and wallpaper strategy. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-29 + * @version 0.1 + * @since 0.19.0 + * @ingroup platform_embedded + */ + +#include "embedded_display_size_policy.h" +#include "embedded_display_server_backend.h" + +#include "IDesktopPropertyStrategy.h" +#include "components/IShellLayerStrategy.h" +#include "components/shell_layer_impl/WidgetShellLayer.h" +#include "components/shell_layer_impl/wallpaper_setup.h" +#include "display_backend_helper.h" +#include "platform_helper.h" +#include "shell_layer_helper.h" + +#include + +namespace cf::desktop::platform_strategy { + +PlatformFactoryAPI native_impl() { + PlatformFactoryAPI api; + // The embedded target owns a single shared size policy (function-local + // static). create() lends a borrowed pointer; release() is a no-op. + api.creator_func = [](IDesktopPropertyStrategy::StrategyType t) + -> IDesktopPropertyStrategy* { + if (t != IDesktopPropertyStrategy::StrategyType::DisplaySizePolicy) { + return nullptr; + } + static auto policy = std::make_unique(); + return policy.get(); + }; + api.release_func = [](IDesktopPropertyStrategy* /*policy*/) { + // Factory-owned; nothing to release. + }; + return api; +} + +} // namespace cf::desktop::platform_strategy + +namespace cf::desktop::platform { + +DisplayBackendFactoryAPI native_display_impl() { + DisplayBackendFactoryAPI api; + api.creator_func = []() -> IDisplayServerBackend* { + return new backend::embedded::EmbeddedDisplayServerBackend(); + }; + api.release_func = [](IDisplayServerBackend* p) { delete p; }; + return api; +} + +ShellLayerFactoryAPI native_shell_layer_impl() { + ShellLayerFactoryAPI api; + api.shell_creator = [](QWidget* parent) -> IShellLayer* { + return new WidgetShellLayer(parent); + }; + api.shell_releaser = [](IShellLayer* p) { delete p; }; + api.strategy_creator = []() -> std::unique_ptr { + return wallpaper::create_wallpaper_strategy(); + }; + return api; +} + +} // namespace cf::desktop::platform diff --git a/desktop/ui/platform/embedded/null_window_backend.cpp b/desktop/ui/platform/embedded/null_window_backend.cpp new file mode 100644 index 000000000..437d106fa --- /dev/null +++ b/desktop/ui/platform/embedded/null_window_backend.cpp @@ -0,0 +1,42 @@ +/** + * @file null_window_backend.cpp + * @brief No-op IWindowBackend implementation for embedded DirectRender mode. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-29 + * @version 0.1 + * @since 0.19.0 + * @ingroup platform_embedded + */ + +#include "null_window_backend.h" + +namespace cf::desktop::backend::embedded { + +NullWindowBackend::NullWindowBackend(QObject* parent) : IWindowBackend(parent) {} + +NullWindowBackend::~NullWindowBackend() = default; + +aex::WeakPtr NullWindowBackend::createWindow(const QString& /*appId*/) { + return {}; +} + +void NullWindowBackend::destroyWindow(aex::WeakPtr /*window*/) {} + +QList> NullWindowBackend::windows() const { + return {}; +} + +render::BackendCapabilities NullWindowBackend::capabilities() const { + // The framebuffer target has no GPU and no multi-window support. + render::BackendCapabilities caps; + caps.supportsMultiWindow = false; + caps.supportsTransparency = false; + caps.hasHardwareAcceleration = false; + caps.supportsVSync = false; + caps.supportsScreenshot = false; + caps.maxTextureSize = 0; + return caps; +} + +} // namespace cf::desktop::backend::embedded diff --git a/desktop/ui/platform/embedded/null_window_backend.h b/desktop/ui/platform/embedded/null_window_backend.h new file mode 100644 index 000000000..213082db8 --- /dev/null +++ b/desktop/ui/platform/embedded/null_window_backend.h @@ -0,0 +1,101 @@ +/** + * @file null_window_backend.h + * @brief No-op IWindowBackend for embedded DirectRender mode. + * + * In DirectRender mode CFDesktop renders its own shell and does not manage + * external application windows. NullWindowBackend satisfies the IWindowBackend + * contract with empty implementations so the shell boots without a windowing + * system. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-29 + * @version 0.1 + * @since 0.19.0 + * @ingroup platform_embedded + */ + +#pragma once + +#include "../../components/IWindowBackend.h" + +namespace cf::desktop::backend::embedded { + +/** + * @brief No-op window backend for embedded DirectRender mode. + * + * Reports no managed windows and limited capabilities. createWindow() returns + * an empty reference and no signals are emitted. + * + * @ingroup platform_embedded + */ +class NullWindowBackend : public IWindowBackend { + Q_OBJECT + public: + /** + * @brief Constructs the null window backend. + * @param[in] parent Optional Qt parent. + * @throws None + * @note None + * @warning None + * @since 0.19.0 + * @ingroup platform_embedded + */ + explicit NullWindowBackend(QObject* parent = nullptr); + + /** + * @brief Destroys the null window backend. + * @throws None + * @note None + * @warning None + * @since 0.19.0 + * @ingroup platform_embedded + */ + ~NullWindowBackend() override; + + /** + * @brief Returns an empty reference; no windows are created. + * @param[in] appId Logical identifier of the application (unused). + * @return Empty weak reference (no window is created). + * @throws None + * @note None + * @warning None + * @since 0.19.0 + * @ingroup platform_embedded + */ + aex::WeakPtr createWindow(const QString& appId) override; + + /** + * @brief No-op destruction; nothing is tracked. + * @param[in] window Weak reference to the window (unused). + * @throws None + * @note None + * @warning None + * @since 0.19.0 + * @ingroup platform_embedded + */ + void destroyWindow(aex::WeakPtr window) override; + + /** + * @brief Returns an empty list; no windows are tracked. + * @return Empty list of weak window references. + * @throws None + * @note None + * @warning None + * @since 0.19.0 + * @ingroup platform_embedded + */ + QList> windows() const override; + + /** + * @brief Returns limited capabilities (no multi-window, no GPU). + * @return BackendCapabilities describing the embedded framebuffer. + * @throws None + * @note None + * @warning None + * @since 0.19.0 + * @ingroup platform_embedded + */ + render::BackendCapabilities capabilities() const override; +}; + +} // namespace cf::desktop::backend::embedded From dd18ca7294f94dce9980c88d22ebafdd7884d341 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Mon, 29 Jun 2026 23:16:53 +0800 Subject: [PATCH 2/4] fix(console): treat degenerate (<=0) console size as unknown ConsoleHelper::size() could return a valid optional holding (0, 0) on consoles with no window size (e.g. an i.MX6ULL serial console). Callers only checked for nullopt, so the zero size reached the early welcome renderer, which built a std::string of static_cast(-2) chars and threw std::length_error, aborting boot. Return nullopt for non-positive dimensions so callers fall back to fallback_size() (80x24). Confirmed: CFDesktop now boots to the shell on i.MX6ULL linuxfb. --- base/device/console/console_helper.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/base/device/console/console_helper.cpp b/base/device/console/console_helper.cpp index 5d2fad923..555ddee5f 100644 --- a/base/device/console/console_helper.cpp +++ b/base/device/console/console_helper.cpp @@ -9,7 +9,18 @@ std::optional ConsoleHelper::size() const { if (!size_querier) { return {}; } - return size_querier->execute(); + auto result = size_querier->execute(); + if (!result) { + return {}; + } + // A degenerate size (non-positive width or height) means the console size + // is unknown -- e.g. a serial console that reports no window size. Treat it + // as unknown so callers fall back to fallback_size() instead of rendering + // into a zero-sized buffer (which throws std::length_error downstream). + if (result->first <= 0 || result->second <= 0) { + return {}; + } + return result; } bool ConsoleHelper::query_property(const std::string_view property, std::any* value) { From da69ba131602aab3546208b3a7cdea131ba98f3f Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Tue, 30 Jun 2026 00:02:35 +0800 Subject: [PATCH 3/4] fix(launcher): make the start-menu popup work on linuxfb (no WM) Four issues blocked the AppLauncher start-menu on the i.MX6ULL linuxfb target (no compositor / window manager): 1. Qt::Popup auto-closes on deactivation: as a top-level popup it was dismissed shortly after show() because linuxfb cannot keep a second window active. Make it a frameless CHILD widget of the desktop so it renders inside the single desktop window -- no activation fight. 2. As a child widget it auto-shows when the desktop becomes visible, rendering the tiles at the default (0,0) on boot. Hide it in the ctor so it only appears on popup(). 3. popup() used setFixedSize + move separately, which could leave a never-shown child at (0,0) on first realization. Use setGeometry (atomic) and force the grid to activate. 4. The start button only ever called popup() (no close path), so a second click was a no-op. Make it a proper toggle (hide if showing, else popup). Confirmed on i.MX6ULL linuxfb: the start menu opens centered above the taskbar, stays open, and toggles cleanly on repeated Start clicks. --- desktop/ui/CFDesktopEntity.cpp | 12 +++++++++- .../ui/components/launcher/app_launcher.cpp | 22 +++++++++++++++---- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/desktop/ui/CFDesktopEntity.cpp b/desktop/ui/CFDesktopEntity.cpp index f3c0f9730..1a4bfa27c 100644 --- a/desktop/ui/CFDesktopEntity.cpp +++ b/desktop/ui/CFDesktopEntity.cpp @@ -257,7 +257,17 @@ CFDesktopEntity::RunsSetupResult CFDesktopEntity::run_init(RunsSetupMethod m) { launch_app); QObject::connect( taskbar, &cf::desktop::desktop_component::CenteredTaskbar::launcherRequested, this, - [app_launcher, panel_mgr]() { app_launcher->popup(panel_mgr->availableGeometry()); }); + [app_launcher, panel_mgr]() { + // Toggle: clicking Start while the launcher is open dismisses it, + // otherwise pop it up. The start button used to only ever call + // popup(), so a second click was a no-op while the menu was already + // visible. + if (app_launcher->isShowing()) { + app_launcher->hideLauncher(); + } else { + app_launcher->popup(panel_mgr->availableGeometry()); + } + }); taskbar->show(); panel_mgr->relayout(); diff --git a/desktop/ui/components/launcher/app_launcher.cpp b/desktop/ui/components/launcher/app_launcher.cpp index 4e057f81b..4ac9d0036 100644 --- a/desktop/ui/components/launcher/app_launcher.cpp +++ b/desktop/ui/components/launcher/app_launcher.cpp @@ -49,12 +49,24 @@ constexpr int kMaxHeight = 540; ///< Maximum popup height (px). } // namespace AppLauncher::AppLauncher(QWidget* parent) : QWidget(parent) { - setWindowFlags(Qt::Popup | Qt::FramelessWindowHint); + // Frameless CHILD widget -- deliberately NOT Qt::Popup. On windowless + // targets (linuxfb, no compositor / window manager) a Qt::Popup is a + // separate top-level window: show() activates it briefly, then the platform + // deactivates it (~0.5s later) and Qt::Popup auto-closes on deactivation, so + // the start menu flashed and vanished. As a child of the desktop it renders + // inside the single desktop window -- no activation fight, no auto-close. + // Dismissal is via the start-button toggle, ESC, and tile clicks. + setWindowFlags(Qt::FramelessWindowHint); setAttribute(Qt::WA_TranslucentBackground); setAttribute(Qt::WA_OpaquePaintEvent, false); setAutoFillBackground(false); setupUi(); applyTheme(); + // Stay hidden until popup() is called. As a child of the desktop (not a + // top-level Qt::Popup), Qt would otherwise auto-show this widget when the + // desktop becomes visible -- rendering the tiles at the default (0,0) + // geometry on boot, before the user ever clicks Start. + hide(); // Follow live theme switches (ThemeManager is the canonical source). connect(&qw::core::ThemeManager::instance(), &qw::core::ThemeManager::themeChanged, this, @@ -82,11 +94,13 @@ void AppLauncher::popup(const QRect& available) { const int x = avail.center().x() - w / 2; const int y = avail.bottom() - h; // Bottom-aligned: sits just above the taskbar. - setFixedSize(w, h); - move(x, y); + // Apply geometry atomically and force the grid to lay tiles out within it. + setGeometry(x, y, w, h); + if (grid_ != nullptr) { + grid_->activate(); + } show(); raise(); - activateWindow(); } void AppLauncher::hideLauncher() { From 853694f40f6e3cc7e2a4e495ed7f61e81c0c4bd9 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Tue, 30 Jun 2026 09:59:41 +0800 Subject: [PATCH 4/4] feat(launcher): config-driven apps.json + builtin About panel The default app list was a set of placeholder X11 apps (xdg-open, xterm, cfdesktop-settings, ...) that do not exist on the i.MX6ULL board, so clicking a launcher tile launched nothing. Make the list per-board configurable and add one real in-process app. - Load the app list from /../apps.json at startup; fall back to defaultApps() when the file is missing/empty. AppEntry fields are read from a JSON apps[] array, so a board customizes the launcher by dropping an apps.json -- no recompile. exec_command already supported args via QProcess::splitCommand, so apps.json just carries them. - Add a builtin About panel: a frameless child widget shown over the desktop when an entry with exec_command "builtin:about" is clicked, dismissed on a tap. CFDesktopEntity routes "builtin:*" entries in-process; everything else goes through AppLaunchService (QProcess). In-process suits the single-framebuffer linuxfb target, where an external GUI app would fight the desktop for /dev/fb0. Confirmed on i.MX6ULL linuxfb: the start menu lists the apps.json entries, About opens/closes, and external entries launch with arguments. --- desktop/ui/CFDesktopEntity.cpp | 95 +++++++++++--- desktop/ui/components/CMakeLists.txt | 2 + .../ui/components/builtin_apps/CMakeLists.txt | 17 +++ .../components/builtin_apps/about_panel.cpp | 110 ++++++++++++++++ .../ui/components/builtin_apps/about_panel.h | 121 ++++++++++++++++++ 5 files changed, 326 insertions(+), 19 deletions(-) create mode 100644 desktop/ui/components/builtin_apps/CMakeLists.txt create mode 100644 desktop/ui/components/builtin_apps/about_panel.cpp create mode 100644 desktop/ui/components/builtin_apps/about_panel.h diff --git a/desktop/ui/CFDesktopEntity.cpp b/desktop/ui/CFDesktopEntity.cpp index 1a4bfa27c..b558c0ce3 100644 --- a/desktop/ui/CFDesktopEntity.cpp +++ b/desktop/ui/CFDesktopEntity.cpp @@ -10,6 +10,7 @@ #include "components/IDisplayServerBackend.h" #include "components/PanelManager.h" #include "components/WindowManager.h" +#include "components/builtin_apps/about_panel.h" #include "components/launcher/app_launch_service.h" #include "components/launcher/app_launcher.h" #include "components/statusbar/status_bar.h" @@ -19,12 +20,49 @@ #include "platform/display_backend_helper.h" #include "platform/shell_layer_helper.h" #include "qt_format.h" +#include +#include #include +#include +#include +#include #include #include namespace cf::desktop { +namespace { +/// Loads the per-board app list from /settings/apps.json. Falls back to +/// defaultApps() when the file is missing, unreadable, empty, or has no valid +/// entries -- so a board only needs to drop an apps.json to customize the +/// launcher/taskbar without a recompile. +QList loadAppsConfig() { + // Read from the desktop root (/../apps.json). Kept out of the + // board-written settings/ dir so it stays owner-writable on the NFS root. + const QString path = + QCoreApplication::applicationDirPath() + QStringLiteral("/../apps.json"); + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + return desktop_component::defaultApps(); + } + const QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); + const auto arr = doc.object().value(QStringLiteral("apps")).toArray(); + QList apps; + for (const auto& value : arr) { + const auto o = value.toObject(); + desktop_component::AppEntry entry; + entry.app_id = o.value(QStringLiteral("app_id")).toString(); + entry.display_name = o.value(QStringLiteral("display_name")).toString(); + entry.icon_path = o.value(QStringLiteral("icon_path")).toString(); + entry.exec_command = o.value(QStringLiteral("exec_command")).toString(); + if (!entry.app_id.isEmpty() && !entry.exec_command.isEmpty()) { + apps.append(entry); + } + } + return apps.isEmpty() ? desktop_component::defaultApps() : apps; +} +} // namespace + std::unique_ptr CFDesktopEntity::global_instance_; CFDesktopEntity& CFDesktopEntity::instance() { @@ -220,8 +258,8 @@ CFDesktopEntity::RunsSetupResult CFDesktopEntity::run_init(RunsSetupMethod m) { // ── Taskbar: bottom-edge panel (centered app icons) ── // apps is captured by the click handler to resolve app_id -> exec_command. - const QList apps = - cf::desktop::desktop_component::defaultApps(); + // Loaded from settings/apps.json (per-board app list) if present. + const QList apps = loadAppsConfig(); auto* taskbar = new cf::desktop::desktop_component::CenteredTaskbar(desktop_entity_); taskbar->setApps(apps); taskbar->setBackdropSource(shell); @@ -229,24 +267,43 @@ CFDesktopEntity::RunsSetupResult CFDesktopEntity::run_init(RunsSetupMethod m) { // Shared launch path: resolve app_id -> exec, launch, capture PID. Used by // both the taskbar tile click and the launcher popup so the running-state // indicator lights for either entry point. - std::function launch_app = [apps, app_pid](const QString& app_id) { - QString exec; - for (const auto& app : apps) { - if (app.app_id == app_id) { - exec = app.exec_command; - break; + // Builtin in-process apps live as hidden child widgets of the desktop and + // are shown when their "builtin:*" exec_command is launched (no QProcess, + // so no framebuffer fight with the desktop on linuxfb). + auto* about_panel = new cf::desktop::desktop_component::AboutPanel(desktop_entity_); + + std::function launch_app = + [apps, app_pid, about_panel, panel_mgr](const QString& app_id) { + QString exec; + for (const auto& app : apps) { + if (app.app_id == app_id) { + exec = app.exec_command; + break; + } } - } - if (exec.isEmpty()) { - cf::log::warningftag("CFDesktopEntity", "No exec for app_id '{}'", - app_id.toStdString()); - return; - } - const auto launched = cf::desktop::desktop_component::AppLaunchService::launch(exec); - if (launched.has_value()) { - (*app_pid)[app_id] = *launched; - } - }; + if (exec.isEmpty()) { + cf::log::warningftag("CFDesktopEntity", "No exec for app_id '{}'", + app_id.toStdString()); + return; + } + // Builtin apps render in-process. External apps are launched + // detached (Stage 2 will add hide-desktop + managed-QProcess for + // GUI apps that need the full framebuffer). + if (exec.startsWith(QStringLiteral("builtin:"))) { + const auto id = exec.mid(QStringLiteral("builtin:").size()); + if (id == QStringLiteral("about") && about_panel != nullptr) { + about_panel->popup(panel_mgr->availableGeometry()); + } else { + cf::log::warningftag("CFDesktopEntity", "Unknown builtin app '{}'", + id.toStdString()); + } + return; + } + const auto launched = cf::desktop::desktop_component::AppLaunchService::launch(exec); + if (launched.has_value()) { + (*app_pid)[app_id] = *launched; + } + }; QObject::connect(taskbar, &cf::desktop::desktop_component::CenteredTaskbar::appClicked, this, launch_app); diff --git a/desktop/ui/components/CMakeLists.txt b/desktop/ui/components/CMakeLists.txt index f459ea3d9..3abdfe423 100644 --- a/desktop/ui/components/CMakeLists.txt +++ b/desktop/ui/components/CMakeLists.txt @@ -10,6 +10,7 @@ add_subdirectory(window_placement) add_subdirectory(statusbar) add_subdirectory(taskbar) add_subdirectory(launcher) +add_subdirectory(builtin_apps) # Create interface library for convenience add_library(cf_desktop_components STATIC) @@ -38,6 +39,7 @@ PRIVATE cfdesktop_statusbar cfdesktop_taskbar cfdesktop_launcher + cfdesktop_builtin_apps cfdesktop_window_placement Qt6::Core Qt6::Gui Qt6::Widgets ) diff --git a/desktop/ui/components/builtin_apps/CMakeLists.txt b/desktop/ui/components/builtin_apps/CMakeLists.txt new file mode 100644 index 000000000..c66492895 --- /dev/null +++ b/desktop/ui/components/builtin_apps/CMakeLists.txt @@ -0,0 +1,17 @@ +# Builtin in-process apps (child widgets over the desktop, e.g. About). +# These are NOT launched via QProcess; CFDesktop shows them directly, which +# suits single-framebuffer targets (linuxfb) where an external GUI app would +# fight the desktop for /dev/fb0. +add_library(cfdesktop_builtin_apps STATIC + about_panel.cpp +) + +target_include_directories(cfdesktop_builtin_apps PUBLIC + $ + $ +) + +target_link_libraries(cfdesktop_builtin_apps +PRIVATE + Qt6::Core Qt6::Gui Qt6::Widgets +) diff --git a/desktop/ui/components/builtin_apps/about_panel.cpp b/desktop/ui/components/builtin_apps/about_panel.cpp new file mode 100644 index 000000000..424c310bf --- /dev/null +++ b/desktop/ui/components/builtin_apps/about_panel.cpp @@ -0,0 +1,110 @@ +/** + * @file about_panel.cpp + * @brief In-process "About CFDesktop" panel implementation. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-30 + * @version 0.1 + * @since 0.20 + * @ingroup components + */ + +#include "about_panel.h" + +#include +#include +#include +#include +#include +#include + +#include + +namespace cf::desktop::desktop_component { + +namespace { +constexpr qreal kCornerRadius = 16.0; ///< Card corner radius (px). +constexpr qreal kWidthRatio = 0.42; ///< Card width as a fraction of available. +constexpr qreal kHeightRatio = 0.36; ///< Card height as a fraction of available. +constexpr int kMinWidth = 360; ///< Minimum card width (px). +constexpr int kMaxWidth = 560; ///< Maximum card width (px). +constexpr int kMinHeight = 220; ///< Minimum card height (px). +constexpr int kMaxHeight = 340; ///< Maximum card height (px). +} // namespace + +AboutPanel::AboutPanel(QWidget* parent) + : QWidget(parent), + version_text_(QStringLiteral("CFDesktop v0.19.0")), + target_text_(QStringLiteral("Running on i.MX6ULL (Qt linuxfb)")) { + setWindowFlags(Qt::FramelessWindowHint); + setAttribute(Qt::WA_TranslucentBackground); + setAttribute(Qt::WA_OpaquePaintEvent, false); + setAutoFillBackground(false); + // Stay hidden until popup(). As a child of the desktop, Qt would otherwise + // auto-show this widget when the desktop becomes visible. + hide(); +} + +AboutPanel::~AboutPanel() = default; + +void AboutPanel::popup(const QRect& available) { + QRect avail = available; + if (!avail.isValid() || avail.width() <= 0 || avail.height() <= 0) { + if (const auto* screen = QGuiApplication::primaryScreen()) { + avail = screen->availableGeometry(); + } + } + + const int w = std::clamp(static_cast(avail.width() * kWidthRatio), kMinWidth, kMaxWidth); + const int h = std::clamp(static_cast(avail.height() * kHeightRatio), kMinHeight, kMaxHeight); + const int x = avail.center().x() - w / 2; + const int y = avail.center().y() - h / 2; + + setGeometry(x, y, w, h); + show(); + raise(); +} + +void AboutPanel::hidePanel() { + hide(); +} + +void AboutPanel::paintEvent(QPaintEvent* /*event*/) { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing, true); + + // Hardcoded Material-ish palette; this demo panel does not track the theme. + const QColor surface_color(0xF7, 0xF5, 0xF3); + const QColor text_color(0x1C, 0x1B, 0x1F); + const QColor hint_color(0x49, 0x45, 0x4E); + + QPainterPath surface; + surface.addRoundedRect(QRectF(rect()), kCornerRadius, kCornerRadius); + p.fillPath(surface, surface_color); + + // Version (title), centered in the upper portion. + QFont title_font = font(); + title_font.setPointSize(18); + title_font.setBold(true); + p.setFont(title_font); + p.setPen(text_color); + p.drawText(QRect(0, 0, width(), height() * 2 / 3), Qt::AlignCenter, version_text_); + + // Target line + dismiss hint, centered in the lower portion. + QFont body_font = font(); + body_font.setPointSize(11); + p.setFont(body_font); + p.setPen(hint_color); + p.drawText(QRect(0, height() / 2, width(), height() / 4), Qt::AlignCenter, target_text_); + p.drawText(QRect(0, height() * 3 / 4, width(), height() / 6), Qt::AlignCenter, + QStringLiteral("(tap to close)")); +} + +void AboutPanel::mouseReleaseEvent(QMouseEvent* event) { + if (event->button() == Qt::LeftButton) { + hidePanel(); + } + QWidget::mouseReleaseEvent(event); +} + +} // namespace cf::desktop::desktop_component diff --git a/desktop/ui/components/builtin_apps/about_panel.h b/desktop/ui/components/builtin_apps/about_panel.h new file mode 100644 index 000000000..5ffd3e3fb --- /dev/null +++ b/desktop/ui/components/builtin_apps/about_panel.h @@ -0,0 +1,121 @@ +/** + * @file about_panel.h + * @brief In-process "About CFDesktop" panel (builtin app). + * + * AboutPanel is a frameless child widget shown over the desktop when the + * "builtin:about" app entry is launched. It renders a rounded Material card + * with the CFDesktop version and the running target, and dismisses on a tap. + * As a child of the desktop it renders inside the single desktop window, so it + * does not fight an external app for the framebuffer on linuxfb. + * + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @date 2026-06-30 + * @version 0.1 + * @since 0.20 + * @ingroup components + */ + +#pragma once + +#include +#include + +class QMouseEvent; +class QPaintEvent; +class QRect; + +namespace cf::desktop::desktop_component { + +/** + * @brief Builtin "About" panel shown over the desktop. + * + * Demonstrates an in-process (non-QProcess) app: clicking the matching launcher + * tile shows this panel; a tap anywhere hides it. No external binary is needed, + * which suits the single-framebuffer linuxfb target. + * + * @ingroup components + */ +class AboutPanel final : public QWidget { + Q_OBJECT + public: + /** + * @brief Constructs the About panel. + * + * @param[in] parent Owning widget (the desktop surface). + * + * @throws None + * @note The panel starts hidden; call popup() to show it. + * @warning None + * @since 0.20 + * @ingroup components + */ + explicit AboutPanel(QWidget* parent = nullptr); + + /** + * @brief Destructs the panel. + * + * @throws None + * @note None + * @warning None + * @since 0.20 + * @ingroup components + */ + ~AboutPanel() override; + + /** + * @brief Sizes, centers over the available area, and shows the panel. + * + * @param[in] available The free screen geometry (excludes docked panels). + * + * @throws None + * @note A null/empty rect centers on the primary screen. + * @warning None + * @since 0.20 + * @ingroup components + */ + void popup(const QRect& available); + + /** + * @brief Hides the panel. + * + * @throws None + * @note None + * @warning None + * @since 0.20 + * @ingroup components + */ + void hidePanel(); + + protected: + /** + * @brief Paints the rounded card with the version and target text. + * + * @param[in] event The paint event descriptor. + * + * @throws None + * @note None + * @warning None + * @since 0.20 + * @ingroup components + */ + void paintEvent(QPaintEvent* event) override; + + /** + * @brief Dismisses the panel on a left-button release. + * + * @param[in] event The mouse event descriptor. + * + * @throws None + * @note None + * @warning None + * @since 0.20 + * @ingroup components + */ + void mouseReleaseEvent(QMouseEvent* event) override; + + private: + QString version_text_; ///< First line (version). + QString target_text_; ///< Second line (running target). +}; + +} // namespace cf::desktop::desktop_component