refactor(host): gate op-host-native GL stack behind opt-in gl-host feature

Phase 0 of the op-web-daemon extraction. op-host-native now defaults to a
raster-only library: with the new NON-default gl-host feature off, it pulls no
winit/glutin/jian-host-desktop/accesskit and skia-safe WITHOUT gl (no GL link
directives). GUI consumers (op-host-desktop) opt in via features=[gl-host], so
the future headless op-web-daemon can depend on op-host-native
(default-features=false) for the raster export path without the desktop GUI stack.

- Move NativeFrameBackend widget_host/ -> backend/ (raster; no longer drags the
  interactive host into the raster build).
- Gate widget_host/preview/boolean_ops/canvas_view_stub + GlutinProvider /
  SharedSkiaContext behind gl-host. skia gl arrives only via skia-safe/gl;
  accesskit made optional. Mobile-safe: default features = no gl-host.

Verified: raster + desktop + app build green; 385 lib tests pass under gl-host;
cargo tree confirms the raster build is winit/glutin/skia-gl free. Codex-approved.
This commit is contained in:
Kayshen-X 2026-06-19 17:47:34 +08:00
parent 7b20644a23
commit 47e9e3cac6
18 changed files with 82 additions and 19 deletions

View file

@ -62,7 +62,7 @@ role = "Editor"
# Phase 7.3 reorg: openpencil-shell-core dissolved — widget facade /
# render-backend / layout scene paths resolve through op-editor-ui.
op-editor-ui = { path = "../op-editor-ui" }
op-host-native = { path = "../op-host-native", version = "0.8.0" }
op-host-native = { path = "../op-host-native", version = "0.8.0", features = ["gl-host"] }
# Accessibility (#67) — the platform-free `accesskit` core types
# (`TreeUpdate` / `ActionRequest` / `ActivationHandler` …) used by the
# desktop a11y adapter (`src/a11y.rs`). The OS-specific subclassing

View file

@ -10,6 +10,40 @@ description = "OpenPencil native host — winit + skia-safe + accesskit backend,
name = "op_host_native"
path = "src/lib.rs"
[features]
# `gl-host` (NON-default) enables the desktop winit/glutin interactive runner
# tier + skia-safe's `gl` feature + accesskit. Consumers that need the GUI host
# (`op-host-desktop`, `op-app`) opt in via `features = ["gl-host"]`. Left OFF —
# the default, used by `op-web-daemon` (via `default-features = false`) and by
# the mobile `cargo check` (default features) — it yields a raster-only library:
# no winit / glutin / jian-host-desktop / accesskit, and skia-safe WITHOUT `gl`
# (so it emits no native GL link directives). The raster `NativeBackend` +
# `NativeFrameBackend` + offscreen raster + text measurement remain. Mobile is
# safe by construction: default features = no `gl-host`; the gated deps also live
# only in the desktop-only target block.
#
# Why non-default (Approach Y): cargo can't alias skia-safe twice ("multiple
# times with different names") and can't make it optional-in-one-target-block +
# required-in-another; and a `skia-safe/gl` edge in a *default* feature would
# flip gl on for mobile. Making `gl-host` non-default + opt-in is the only
# cargo-valid, mobile-safe way to keep skia's `gl` out of the raster build.
default = []
gl-host = [
"skia-safe/gl",
"dep:glutin",
"dep:winit",
"dep:scopeguard",
"dep:jian-host-desktop",
"dep:accesskit",
]
# The basic_window demo needs the GL host stack (SharedSkiaContext + winit);
# `required-features` makes `cargo build --examples` skip it under
# `--no-default-features` instead of failing on a missing `main`.
[[example]]
name = "basic_window"
required-features = ["gl-host"]
[dependencies]
# Phase 7.3 reorg: openpencil-shell-core dissolved — widget facade /
# theme / layout scene / scene vars / render-backend / gesture types
@ -20,7 +54,9 @@ op-editor-ui = { path = "../op-editor-ui" }
# into an `accesskit::TreeUpdate` (see `widget_host/a11y.rs`) for the
# desktop platform adapter to publish. Pinned to 0.24 to match
# op-editor-ui + the platform adapter crates' transitive accesskit.
accesskit = "0.24"
# Optional + behind `gl-host` (its only user, `widget_host/a11y.rs`, is
# gl-host-gated) so the raster-only build pulls no accesskit at all.
accesskit = { version = "0.24", optional = true }
# Canvas Preview (Play) mode serializes the document to JSON before
# building the jian runtime, so the saved doc is never mutated.
@ -113,12 +149,11 @@ jian-skia = { path = "../../vendor/jian/crates/jian-skia", version = "0.0.1", fe
# `compile_error!`. macOS / Windows backends auto-enable via
# cfg(target_os) and need no feature flag.
[target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies]
skia-safe = { version = "0.97.0", default-features = false, features = [
"binary-cache",
"textlayout",
"gl",
] }
glutin = "0.32.3"
# skia-safe's `gl` feature is added via the `gl-host` feature (`skia-safe/gl`),
# NOT here — so with `gl-host` off the raster/mobile build links the 5-OS
# skia-safe above WITHOUT `gl` and emits no native GL link directives. The 5-OS
# entry is the single skia-safe edge; no desktop-only skia-safe entry is needed.
glutin = { version = "0.32.3", optional = true }
# `casement` — ZSeven-W's winit fork (vendored as a submodule under
# `vendor/casement` so CI checks out the same source the desktop binary
# links). The `package` key keeps the `winit` import name. `glutin-winit`
@ -127,14 +162,14 @@ glutin = "0.32.3"
# tree. Its only use here — `GlWindow::build_surface_attributes` — is
# replaced by a direct `glutin::surface::SurfaceAttributesBuilder`
# call in `context/provider.rs`.
winit = { package = "casement", path = "../../vendor/casement", default-features = false, features = [
winit = { package = "casement", path = "../../vendor/casement", default-features = false, optional = true, features = [
"x11",
"wayland",
"wayland-csd-adwaita",
"rwh_06",
] }
scopeguard = "1.2"
jian-host-desktop = { path = "../../vendor/jian/crates/jian-host-desktop", version = "0.0.1", default-features = false, features = [
scopeguard = { version = "1.2", optional = true }
jian-host-desktop = { path = "../../vendor/jian/crates/jian-host-desktop", version = "0.0.1", default-features = false, optional = true, features = [
"textlayout",
] }

View file

@ -8,6 +8,8 @@
//! Step 1c+ may wrap `NativeBackend` inside a `WithCanvas<'a>` newtype so
//! the OP `RenderBackend` trait gets a real impl.
mod frame_backend;
pub mod skia;
pub use frame_backend::NativeFrameBackend;
pub use skia::{enumerate_system_font_families, to_jian_rect, NativeBackend};

View file

@ -19,6 +19,7 @@ pub use provider::{GlContextProvider, ProviderError, ProviderResult};
pub use provider::AndroidEglProvider;
#[cfg(target_os = "ios")]
pub use provider::EaglProvider;
#[cfg(feature = "gl-host")]
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
pub use provider::GlutinProvider;
@ -26,7 +27,9 @@ pub use provider::GlutinProvider;
// (cfg-gated dependencies). Step 1f mobile wiring will introduce a mobile
// twin (or generalize this one) once iOS / Android providers go from
// `unimplemented!()` placeholders to real GL contexts.
#[cfg(feature = "gl-host")]
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
mod shared;
#[cfg(feature = "gl-host")]
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
pub use shared::{SharedSkiaContext, SharedSkiaError, SharedSkiaResult, SurfaceConfig};

View file

@ -10,6 +10,7 @@
//! exist as compile-time placeholders so the public API surface is frozen
//! before Step 1f real implementations land.
#[cfg(feature = "gl-host")]
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
use std::error::Error;
use std::sync::Arc;
@ -34,6 +35,7 @@ impl ProviderError {
/// desktop `GlutinProvider` to convert glutin / glutin-winit errors;
/// `cfg(any(...))`-gated to silence `dead_code` on iOS / Android where
/// no in-tree caller exists yet (Step 1f mobile providers will use it).
#[cfg(feature = "gl-host")]
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
pub(crate) fn from_error<E: Error>(err: E) -> Self {
Self::Failure(err.to_string())
@ -118,6 +120,7 @@ pub trait GlContextProvider {
/// All non-`Send` glutin handles live in [`Option`]s so `release` can
/// drop them in a defined order (surface → context → display) without
/// requiring `&mut self` to consume `self`.
#[cfg(feature = "gl-host")]
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
pub struct GlutinProvider {
/// Currently-current context. `None` after `release`.
@ -130,6 +133,7 @@ pub struct GlutinProvider {
glow: Arc<glow::Context>,
}
#[cfg(feature = "gl-host")]
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
impl GlutinProvider {
/// Construct from an existing winit window. Builds a glutin display
@ -236,6 +240,7 @@ impl GlutinProvider {
}
}
#[cfg(feature = "gl-host")]
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
fn window_surface_size(
inner: winit::dpi::PhysicalSize<u32>,
@ -251,6 +256,7 @@ fn window_surface_size(
Ok((width, height))
}
#[cfg(feature = "gl-host")]
#[cfg(target_os = "macos")]
fn pick_display_api(
_raw: raw_window_handle::RawWindowHandle,
@ -258,6 +264,7 @@ fn pick_display_api(
glutin::display::DisplayApiPreference::Cgl
}
#[cfg(feature = "gl-host")]
#[cfg(target_os = "windows")]
fn pick_display_api(
raw: raw_window_handle::RawWindowHandle,
@ -265,6 +272,7 @@ fn pick_display_api(
glutin::display::DisplayApiPreference::WglThenEgl(Some(raw))
}
#[cfg(feature = "gl-host")]
#[cfg(target_os = "linux")]
fn pick_display_api(
_raw: raw_window_handle::RawWindowHandle,
@ -274,6 +282,7 @@ fn pick_display_api(
glutin::display::DisplayApiPreference::Egl
}
#[cfg(feature = "gl-host")]
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
impl GlContextProvider for GlutinProvider {
#[tracing::instrument(skip(self))]
@ -411,7 +420,7 @@ impl GlContextProvider for AndroidEglProvider {
}
}
#[cfg(test)]
#[cfg(all(test, feature = "gl-host"))]
mod tests {
use super::*;

View file

@ -57,6 +57,7 @@ pub mod context;
target_os = "android"
))]
pub mod backend;
#[cfg(feature = "gl-host")]
#[cfg(any(
target_os = "macos",
target_os = "linux",
@ -65,8 +66,10 @@ pub mod backend;
target_os = "android"
))]
pub mod boolean_ops;
#[cfg(feature = "gl-host")]
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
pub mod canvas_view_stub;
#[cfg(feature = "gl-host")]
#[cfg(any(
target_os = "macos",
target_os = "linux",
@ -77,6 +80,7 @@ pub mod canvas_view_stub;
pub mod widget_host;
// Canvas Preview (Play) mode runtime owner — depends on the OP
// `RenderBackend` trait (same gate as `widget_host`).
#[cfg(feature = "gl-host")]
#[cfg(any(
target_os = "macos",
target_os = "linux",
@ -93,7 +97,8 @@ pub mod preview;
target_os = "ios",
target_os = "android"
))]
pub use backend::{to_jian_rect, NativeBackend};
pub use backend::{to_jian_rect, NativeBackend, NativeFrameBackend};
#[cfg(feature = "gl-host")]
#[cfg(any(
target_os = "macos",
target_os = "linux",
@ -102,6 +107,7 @@ pub use backend::{to_jian_rect, NativeBackend};
target_os = "android"
))]
pub use preview::PreviewSession;
#[cfg(feature = "gl-host")]
#[cfg(any(
target_os = "macos",
target_os = "linux",
@ -109,8 +115,9 @@ pub use preview::PreviewSession;
target_os = "ios",
target_os = "android"
))]
pub use widget_host::{CursorHint, NativeFrameBackend, WidgetHostNative};
pub use widget_host::{CursorHint, WidgetHostNative};
// canvas_view_stub stays desktop-only (uses glow GL-isolation probe).
#[cfg(feature = "gl-host")]
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
pub use canvas_view_stub::CanvasViewportStub;
@ -118,6 +125,7 @@ pub use canvas_view_stub::CanvasViewportStub;
pub use context::{GlContextProvider, ProviderError, ProviderResult};
// Desktop-only re-exports.
#[cfg(feature = "gl-host")]
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
pub use context::{
GlutinProvider, SharedSkiaContext, SharedSkiaError, SharedSkiaResult, SurfaceConfig,

View file

@ -22,7 +22,7 @@
//!
//! This file is the public spine. Implementation methods are split
//! across sibling submodules (per the 800-line-per-file ceiling):
//! - [`frame_backend`] — `NativeFrameBackend` (`RenderBackend` impl)
//! - `NativeFrameBackend` (`RenderBackend` impl) — moved to `crate::backend`
//! - [`helpers`] — hex parsing + resize-bounds math + constants
//! - [`geometry`] — canvas region / cursor hint / picker rect helpers
//! - [`input`] — cursor-move / release / panel-resize input handlers
@ -76,7 +76,6 @@ mod design_md_press_tests;
#[cfg(test)]
mod figma_import_tests;
mod font_picker_dispatch;
mod frame_backend;
mod geometry;
mod geometry_settings_hover;
#[cfg(test)]
@ -147,8 +146,6 @@ mod variables_panel_ux_tests;
mod variables_preset_press;
mod viewport_fit;
pub use frame_backend::NativeFrameBackend;
/// Cursor affordance the host suggests for a given screen point.
/// The runner maps each variant to its native cursor (`CursorIcon`
/// on desktop, CSS `cursor:` string on web).

View file

@ -2,11 +2,11 @@
//! Pulled out of `widget_host.rs` to keep the spine file under
//! the 800-line ceiling.
use super::frame_backend::NativeFrameBackend;
use super::helpers::{
GIT_PANEL_CARET_H, GIT_PANEL_CARET_HALF, STATUS_INSET, TOOLBAR_INSET_X, TOOLBAR_INSET_Y,
};
use super::WidgetHostNative;
use crate::backend::NativeFrameBackend;
use op_editor_ui::widgets::editor_state_ext::theme_for;
use op_editor_ui::widgets::{
variables_panel::VariablesPanel, AIChatPlaceholder, AlignToolbar, CanvasViewport,

View file

@ -1,3 +1,4 @@
#![cfg(feature = "gl-host")]
//! Shared test helpers for `op-host-native`.
//!
//! Spec v19 §9 / plan v7 Task 2 Step 16a-16f: this module supplies

View file

@ -1,3 +1,4 @@
#![cfg(feature = "gl-host")]
//! Spec v19 acceptance #2 (chrome + canvas-stub on the same GPU surface)
//! / plan v7 Task 2 Step 16d / round 6 BLOCK-R6-2.
//!

View file

@ -1,3 +1,4 @@
#![cfg(feature = "gl-host")]
//! Spec v19 §9.2 / acceptance #3 / plan v7 Task 2 Step 16f.
//!
//! Per-OS dispatch:

View file

@ -1,3 +1,4 @@
#![cfg(feature = "gl-host")]
//! Spec v19 §9.3 acceptance #6 / plan v7 Task 2 Step 16b:
//! 100 iterations of `{ create + frame + present + teardown × 3 }` must
//! not grow RSS by more than 5 % over baseline.

View file

@ -1,3 +1,4 @@
#![cfg(feature = "gl-host")]
//! Pixel-level proof that Canvas Preview (Play) renders through the
//! design-canvas scene painter — the "orange renders grey / layout
//! scattered" bug, verified end-to-end on a real raster surface.

View file

@ -1,3 +1,4 @@
#![cfg(feature = "gl-host")]
//! Spec v19 acceptance #2 (chrome region) / plan v7 Task 2 Step 16d.
//!
//! Round 5 BLOCK-R5-2 fix: this test covers **chrome only** on a raster

View file

@ -1,3 +1,4 @@
#![cfg(feature = "gl-host")]
//! Spec v19 acceptance #5 (resize / DPI no-flicker) / Phase A Gate
//! round 2 CONCERN 2.
//!

View file

@ -1,3 +1,4 @@
#![cfg(feature = "gl-host")]
//! Spec v19 §3.3 / plan v7 Task 2 Step 16a:
//! `teardown` + lifecycle hooks must be idempotent.
//!

View file

@ -1,3 +1,4 @@
#![cfg(feature = "gl-host")]
//! Spec v19 acceptance #8 / plan v7 Task 2 Step 16c:
//! `tracing` spans must fire at `begin_frame / present / resize /
//! teardown` (and the rest of the frame surface). `tracing-test`