Functional parity with linux
This commit is contained in:
43
Cargo.lock
generated
43
Cargo.lock
generated
@@ -1314,6 +1314,32 @@ dependencies = [
|
|||||||
"objc2",
|
"objc2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-core-graphics"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"dispatch2",
|
||||||
|
"objc2",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
"objc2-io-surface",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-core-video"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"objc2",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
"objc2-core-graphics",
|
||||||
|
"objc2-io-surface",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-encode"
|
name = "objc2-encode"
|
||||||
version = "4.1.0"
|
version = "4.1.0"
|
||||||
@@ -1333,6 +1359,17 @@ dependencies = [
|
|||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-io-surface"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"objc2",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-metal"
|
name = "objc2-metal"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@@ -1352,8 +1389,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
|
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
|
"block2",
|
||||||
|
"libc",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
|
"objc2-core-graphics",
|
||||||
|
"objc2-core-video",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"objc2-metal",
|
"objc2-metal",
|
||||||
]
|
]
|
||||||
@@ -1863,7 +1904,9 @@ dependencies = [
|
|||||||
"objc2",
|
"objc2",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
|
"objc2-quartz-core",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
|
"raw-window-metal",
|
||||||
"ruin-runtime",
|
"ruin-runtime",
|
||||||
"ruin_ui",
|
"ruin_ui",
|
||||||
"ruin_ui_renderer_wgpu",
|
"ruin_ui_renderer_wgpu",
|
||||||
|
|||||||
@@ -282,6 +282,14 @@ impl Reactor {
|
|||||||
self.inner.ensure_flush_scheduled();
|
self.inner.ensure_flush_scheduled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Flushes currently queued reactive jobs immediately on the calling thread.
|
||||||
|
///
|
||||||
|
/// This is useful when host integrations need synchronous propagation
|
||||||
|
/// (for example, during native resize loops).
|
||||||
|
pub fn flush_now(&self) {
|
||||||
|
Rc::clone(&self.inner).flush_jobs();
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn allocate_node(&self) -> NodeId {
|
pub(crate) fn allocate_node(&self) -> NodeId {
|
||||||
let raw = self.inner.next_node.get();
|
let raw = self.inner.next_node.get();
|
||||||
self.inner.next_node.set(raw.wrapping_add(1));
|
self.inner.next_node.set(raw.wrapping_add(1));
|
||||||
|
|||||||
18
lib/ruin_app/build.rs
Normal file
18
lib/ruin_app/build.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-changed=macos/Info.plist");
|
||||||
|
|
||||||
|
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||||
|
if target_os != "macos" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("manifest dir"));
|
||||||
|
let plist = manifest_dir.join("macos/Info.plist");
|
||||||
|
let arg = format!("-Wl,-sectcreate,__TEXT,__info_plist,{}", plist.display());
|
||||||
|
|
||||||
|
// Ensure all ruin_app executables/examples opt into Retina rendering.
|
||||||
|
println!("cargo:rustc-link-arg-examples={arg}");
|
||||||
|
}
|
||||||
@@ -1,7 +1,16 @@
|
|||||||
use ruin_app::prelude::*;
|
use ruin_app::prelude::*;
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
#[ruin_runtime::async_main]
|
#[ruin_runtime::async_main]
|
||||||
async fn main() -> ruin_app::Result<()> {
|
async fn main() -> ruin_app::Result<()> {
|
||||||
|
let _ = tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(
|
||||||
|
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
|
||||||
|
)
|
||||||
|
.with_target(true)
|
||||||
|
.without_time()
|
||||||
|
.try_init();
|
||||||
|
|
||||||
let title = "RUIN Counter";
|
let title = "RUIN Counter";
|
||||||
|
|
||||||
App::new()
|
App::new()
|
||||||
|
|||||||
@@ -90,21 +90,12 @@ fn TaskBoard() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let all_label = if matches!(filter.get(), TaskFilter::All) {
|
let all_label = filter_label(matches!(filter.get(), TaskFilter::All), "All");
|
||||||
"● All"
|
let open_label = filter_label(matches!(filter.get(), TaskFilter::OpenOnly), "Open");
|
||||||
} else {
|
let done_label = filter_label(
|
||||||
"○ All"
|
matches!(filter.get(), TaskFilter::CompletedOnly),
|
||||||
};
|
"Completed",
|
||||||
let open_label = if matches!(filter.get(), TaskFilter::OpenOnly) {
|
);
|
||||||
"● Open"
|
|
||||||
} else {
|
|
||||||
"○ Open"
|
|
||||||
};
|
|
||||||
let done_label = if matches!(filter.get(), TaskFilter::CompletedOnly) {
|
|
||||||
"● Completed"
|
|
||||||
} else {
|
|
||||||
"○ Completed"
|
|
||||||
};
|
|
||||||
let task_rows = visible_ids.with(|ids| {
|
let task_rows = visible_ids.with(|ids| {
|
||||||
ids.iter()
|
ids.iter()
|
||||||
.map(|task_id| {
|
.map(|task_id| {
|
||||||
@@ -466,6 +457,14 @@ fn move_task(tasks: &Signal<Vec<Task>>, task_id: u64, direction: MoveDirection)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn filter_label(active: bool, label: &str) -> String {
|
||||||
|
if active {
|
||||||
|
format!("[x] {label}")
|
||||||
|
} else {
|
||||||
|
format!("[ ] {label}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn seed_tasks() -> Vec<Task> {
|
fn seed_tasks() -> Vec<Task> {
|
||||||
vec![
|
vec![
|
||||||
Task {
|
Task {
|
||||||
|
|||||||
@@ -268,6 +268,7 @@ fn RuntimeIoExample(server_addr: SocketAddr) -> impl IntoView {
|
|||||||
};
|
};
|
||||||
eprintln!("example05: {message}");
|
eprintln!("example05: {message}");
|
||||||
let _ = save_status.set(Some(message));
|
let _ = save_status.set(Some(message));
|
||||||
|
flush_reactive_updates();
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
16
lib/ruin_app/macos/Info.plist
Normal file
16
lib/ruin_app/macos/Info.plist
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>RUIN</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>dev.ruin.example</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -15,8 +15,8 @@ use std::marker::PhantomData;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use ruin_reactivity::effect;
|
use ruin_reactivity::{current as current_reactor, effect};
|
||||||
use ruin_runtime::queue_future;
|
use ruin_runtime::{queue_future, spawn_worker};
|
||||||
use ruin_ui::{
|
use ruin_ui::{
|
||||||
Border, Color, CursorIcon, DisplayItem, Edges, Element, ElementId, HitTarget, InteractionTree,
|
Border, Color, CursorIcon, DisplayItem, Edges, Element, ElementId, HitTarget, InteractionTree,
|
||||||
KeyboardEvent, KeyboardEventKind, KeyboardKey, LayoutCache, LayoutSnapshot, PlatformEvent,
|
KeyboardEvent, KeyboardEventKind, KeyboardKey, LayoutCache, LayoutSnapshot, PlatformEvent,
|
||||||
@@ -26,7 +26,7 @@ use ruin_ui::{
|
|||||||
layout_snapshot_with_cache,
|
layout_snapshot_with_cache,
|
||||||
};
|
};
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use ruin_ui_platform_macos::start_macos_ui;
|
use ruin_ui_platform_macos::{run_macos_app_event_loop, start_macos_ui};
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use ruin_ui_platform_wayland::start_wayland_ui;
|
use ruin_ui_platform_wayland::start_wayland_ui;
|
||||||
|
|
||||||
@@ -98,8 +98,7 @@ impl App {
|
|||||||
pub fn mount<M: Mountable>(self, root: M) -> MountedApp<M> {
|
pub fn mount<M: Mountable>(self, root: M) -> MountedApp<M> {
|
||||||
MountedApp {
|
MountedApp {
|
||||||
window: self.window,
|
window: self.window,
|
||||||
root: Rc::new(root),
|
root,
|
||||||
render_state: Rc::new(RenderState::default()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,19 +149,18 @@ impl Mountable for Element {
|
|||||||
|
|
||||||
pub struct MountedApp<M: Mountable> {
|
pub struct MountedApp<M: Mountable> {
|
||||||
window: Window,
|
window: Window,
|
||||||
root: Rc<M>,
|
root: M,
|
||||||
render_state: Rc<RenderState>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<M: Mountable> MountedApp<M> {
|
impl<M: Mountable> MountedApp<M> {
|
||||||
pub async fn run(self) -> Result<()> {
|
async fn run_with_ui_runtime(
|
||||||
let MountedApp {
|
window: Window,
|
||||||
window: app_window,
|
root: M,
|
||||||
root,
|
mut ui: ruin_ui::UiRuntime,
|
||||||
render_state,
|
) -> Result<()> {
|
||||||
} = self;
|
let root = Rc::new(root);
|
||||||
|
let render_state = Rc::new(RenderState::default());
|
||||||
let mut ui = start_platform_ui();
|
let app_window = window;
|
||||||
let window = ui.create_window(app_window.spec.clone())?;
|
let window = ui.create_window(app_window.spec.clone())?;
|
||||||
let initial_viewport = app_window
|
let initial_viewport = app_window
|
||||||
.spec
|
.spec
|
||||||
@@ -170,6 +168,7 @@ impl<M: Mountable> MountedApp<M> {
|
|||||||
.unwrap_or(UiSize::new(960.0, 640.0));
|
.unwrap_or(UiSize::new(960.0, 640.0));
|
||||||
|
|
||||||
let viewport = ruin_reactivity::cell(initial_viewport);
|
let viewport = ruin_reactivity::cell(initial_viewport);
|
||||||
|
let configure_serial = ruin_reactivity::cell(0_u64);
|
||||||
let scene_version = StdCell::new(0_u64);
|
let scene_version = StdCell::new(0_u64);
|
||||||
let text_system = Rc::new(RefCell::new(TextSystem::new()));
|
let text_system = Rc::new(RefCell::new(TextSystem::new()));
|
||||||
let layout_cache = Rc::new(RefCell::new(LayoutCache::new()));
|
let layout_cache = Rc::new(RefCell::new(LayoutCache::new()));
|
||||||
@@ -183,6 +182,7 @@ impl<M: Mountable> MountedApp<M> {
|
|||||||
let _scene_effect = effect({
|
let _scene_effect = effect({
|
||||||
let window = window.clone();
|
let window = window.clone();
|
||||||
let viewport = viewport.clone();
|
let viewport = viewport.clone();
|
||||||
|
let configure_serial = configure_serial.clone();
|
||||||
let text_system = Rc::clone(&text_system);
|
let text_system = Rc::clone(&text_system);
|
||||||
let layout_cache = Rc::clone(&layout_cache);
|
let layout_cache = Rc::clone(&layout_cache);
|
||||||
let interaction_tree = Rc::clone(&interaction_tree);
|
let interaction_tree = Rc::clone(&interaction_tree);
|
||||||
@@ -194,6 +194,7 @@ impl<M: Mountable> MountedApp<M> {
|
|||||||
let text_selection = Rc::clone(&input_state.text_selection);
|
let text_selection = Rc::clone(&input_state.text_selection);
|
||||||
move || {
|
move || {
|
||||||
let viewport = viewport.get();
|
let viewport = viewport.get();
|
||||||
|
let _ = configure_serial.get();
|
||||||
let version = scene_version.get().wrapping_add(1);
|
let version = scene_version.get().wrapping_add(1);
|
||||||
scene_version.set(version);
|
scene_version.set(version);
|
||||||
let _ = text_selection.version.get();
|
let _ = text_selection.version.get();
|
||||||
@@ -266,6 +267,8 @@ impl<M: Mountable> MountedApp<M> {
|
|||||||
"app received Configured, queuing layout effect"
|
"app received Configured, queuing layout effect"
|
||||||
);
|
);
|
||||||
let _ = viewport.set(configuration.actual_inner_size);
|
let _ = viewport.set(configuration.actual_inner_size);
|
||||||
|
configure_serial.update(|value| *value = value.wrapping_add(1));
|
||||||
|
current_reactor().flush_now();
|
||||||
}
|
}
|
||||||
PlatformEvent::Pointer { window_id, event } if window_id == window.id() => {
|
PlatformEvent::Pointer { window_id, event } if window_id == window.id() => {
|
||||||
Self::handle_pointer_event(
|
Self::handle_pointer_event(
|
||||||
@@ -276,6 +279,7 @@ impl<M: Mountable> MountedApp<M> {
|
|||||||
&mut input_state,
|
&mut input_state,
|
||||||
event,
|
event,
|
||||||
)?;
|
)?;
|
||||||
|
current_reactor().flush_now();
|
||||||
}
|
}
|
||||||
PlatformEvent::Keyboard { window_id, event } if window_id == window.id() => {
|
PlatformEvent::Keyboard { window_id, event } if window_id == window.id() => {
|
||||||
Self::handle_keyboard_event(
|
Self::handle_keyboard_event(
|
||||||
@@ -285,11 +289,22 @@ impl<M: Mountable> MountedApp<M> {
|
|||||||
&input_state,
|
&input_state,
|
||||||
event,
|
event,
|
||||||
)?;
|
)?;
|
||||||
|
current_reactor().flush_now();
|
||||||
}
|
}
|
||||||
PlatformEvent::CloseRequested { window_id } if window_id == window.id() => {
|
PlatformEvent::CloseRequested { window_id } if window_id == window.id() => {
|
||||||
|
tracing::debug!(
|
||||||
|
target: "ruin_app::lifecycle",
|
||||||
|
?window_id,
|
||||||
|
"app received CloseRequested, issuing open(false)"
|
||||||
|
);
|
||||||
let _ = window.update(WindowUpdate::new().open(false));
|
let _ = window.update(WindowUpdate::new().open(false));
|
||||||
}
|
}
|
||||||
PlatformEvent::Closed { window_id } if window_id == window.id() => {
|
PlatformEvent::Closed { window_id } if window_id == window.id() => {
|
||||||
|
tracing::debug!(
|
||||||
|
target: "ruin_app::lifecycle",
|
||||||
|
?window_id,
|
||||||
|
"app received Closed, shutting down ui runtime"
|
||||||
|
);
|
||||||
ui.shutdown()?;
|
ui.shutdown()?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -301,6 +316,14 @@ impl<M: Mountable> MountedApp<M> {
|
|||||||
ui.shutdown()?;
|
ui.shutdown()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<M: Mountable> MountedApp<M> {
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
pub async fn run(self) -> Result<()> {
|
||||||
|
let ui = start_platform_ui();
|
||||||
|
Self::run_with_ui_runtime(self.window, self.root, ui).await
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_pointer_event(
|
fn handle_pointer_event(
|
||||||
window: &WindowController,
|
window: &WindowController,
|
||||||
@@ -453,13 +476,46 @@ impl<M: Mountable> MountedApp<M> {
|
|||||||
if selection_changed {
|
if selection_changed {
|
||||||
text_selection.version.update(|value| *value += 1);
|
text_selection.version.update(|value| *value += 1);
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
sync_primary_selection(_window, interaction_tree, *text_selection.selection.borrow())?;
|
sync_primary_selection(
|
||||||
|
_window,
|
||||||
|
interaction_tree,
|
||||||
|
*text_selection.selection.borrow(),
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
impl<M: Mountable + Send + 'static> MountedApp<M> {
|
||||||
|
pub async fn run(self) -> Result<()> {
|
||||||
|
let ui = start_platform_ui();
|
||||||
|
let (done_tx, done_rx) = std::sync::mpsc::channel::<std::result::Result<(), String>>();
|
||||||
|
let window = self.window;
|
||||||
|
let root = self.root;
|
||||||
|
let _worker = spawn_worker(
|
||||||
|
move || {
|
||||||
|
queue_future(async move {
|
||||||
|
let result = Self::run_with_ui_runtime(window, root, ui)
|
||||||
|
.await
|
||||||
|
.map_err(|err| err.to_string());
|
||||||
|
let _ = done_tx.send(result);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|| {},
|
||||||
|
);
|
||||||
|
|
||||||
|
run_macos_app_event_loop();
|
||||||
|
|
||||||
|
match done_rx.recv() {
|
||||||
|
Ok(Ok(())) => Ok(()),
|
||||||
|
Ok(Err(err)) => Err(err.into()),
|
||||||
|
Err(_) => Err("ui worker terminated before completion".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct View {
|
pub struct View {
|
||||||
element: Element,
|
element: Element,
|
||||||
@@ -1307,6 +1363,10 @@ pub fn use_effect(effect: impl Fn() + 'static) {
|
|||||||
with_hook_slot(|| ruin_reactivity::effect(effect), |_| ());
|
with_hook_slot(|| ruin_reactivity::effect(effect), |_| ());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn flush_reactive_updates() {
|
||||||
|
current_reactor().flush_now();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn use_window_title(compute: impl FnOnce() -> String) {
|
pub fn use_window_title(compute: impl FnOnce() -> String) {
|
||||||
with_render_context_state(|context| {
|
with_render_context_state(|context| {
|
||||||
context.side_effects.borrow_mut().window_title = Some(compute());
|
context.side_effects.borrow_mut().window_title = Some(compute());
|
||||||
@@ -1520,6 +1580,7 @@ where
|
|||||||
let result = future.await;
|
let result = future.await;
|
||||||
if generation.get() == next_generation {
|
if generation.get() == next_generation {
|
||||||
let _ = resource.state.replace(ResourceState::Ready(result));
|
let _ = resource.state.replace(ResourceState::Ready(result));
|
||||||
|
current_reactor().flush_now();
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -1997,10 +2058,10 @@ pub mod prelude {
|
|||||||
ContextKey, FocusScope, FontWeight, IntoBorder, IntoEdges, IntoView, Key, Memo, Mountable,
|
ContextKey, FocusScope, FontWeight, IntoBorder, IntoEdges, IntoView, Key, Memo, Mountable,
|
||||||
Pending, Ready, Resource, ResourceState, Result, ScrollBoxBuilder, ScrollBoxWidget,
|
Pending, Ready, Resource, ResourceState, Result, ScrollBoxBuilder, ScrollBoxWidget,
|
||||||
Shortcut, ShortcutScope, Signal, TextBuilder, TextChildren, TextRole, TextValue, View,
|
Shortcut, ShortcutScope, Signal, TextBuilder, TextChildren, TextRole, TextValue, View,
|
||||||
WidgetRef, Window, block, button, colors, column, component, context_provider, provide,
|
WidgetRef, Window, block, button, colors, column, component, context_provider,
|
||||||
row, scroll_box, surfaces, text, use_context, use_effect, use_memo, use_resource,
|
flush_reactive_updates, provide, row, scroll_box, surfaces, text, use_context, use_effect,
|
||||||
use_shortcut, use_shortcut_with_context, use_signal, use_widget_ref, use_window_title,
|
use_memo, use_resource, use_shortcut, use_shortcut_with_context, use_signal,
|
||||||
view,
|
use_widget_ref, use_window_title, view,
|
||||||
};
|
};
|
||||||
pub use ruin_ui::{
|
pub use ruin_ui::{
|
||||||
Border, Color, CursorIcon, Edges, Element, ElementId, InteractionTree, PointerButton,
|
Border, Color, CursorIcon, Edges, Element, ElementId, InteractionTree, PointerButton,
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ pub use platform::current::driver::{
|
|||||||
pub use platform::current::runtime::{
|
pub use platform::current::runtime::{
|
||||||
IntervalHandle, JoinHandle, ThreadHandle, TimeoutHandle, WorkerHandle, clear_interval,
|
IntervalHandle, JoinHandle, ThreadHandle, TimeoutHandle, WorkerHandle, clear_interval,
|
||||||
clear_timeout, current_thread_handle, queue_future, queue_microtask, queue_task, run,
|
clear_timeout, current_thread_handle, queue_future, queue_microtask, queue_task, run,
|
||||||
set_interval, set_timeout, spawn_worker, yield_now,
|
run_ready_tasks, run_until_stalled, set_interval, set_timeout, spawn_worker, yield_now,
|
||||||
};
|
};
|
||||||
/// Public mesh-allocator surface re-exported from the Linux x86_64 backend.
|
/// Public mesh-allocator surface re-exported from the Linux x86_64 backend.
|
||||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||||
|
|||||||
@@ -478,6 +478,99 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Drains ready work on the current runtime thread without blocking for future work.
|
||||||
|
///
|
||||||
|
/// Unlike [`run`], this returns as soon as there are no immediately runnable
|
||||||
|
/// microtasks or macrotasks left. It is intended for host integrations that
|
||||||
|
/// need to re-enter the scheduler while an outer platform loop remains active.
|
||||||
|
pub fn run_until_stalled() {
|
||||||
|
let _ = current_thread();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
drain_all();
|
||||||
|
|
||||||
|
let mut microtasks_run: u64 = 0;
|
||||||
|
while let Some(task) = pop_microtask() {
|
||||||
|
task();
|
||||||
|
microtasks_run += 1;
|
||||||
|
drain_all();
|
||||||
|
}
|
||||||
|
if microtasks_run >= MICROTASK_STARVATION_THRESHOLD {
|
||||||
|
tracing::warn!(
|
||||||
|
target: trace_targets::SCHEDULER,
|
||||||
|
event = "microtask_starvation",
|
||||||
|
count = microtasks_run,
|
||||||
|
"microtask queue ran {microtasks_run} tasks in a single turn; macrotask handlers may be starved",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(task) = pop_macrotask() {
|
||||||
|
task();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
drain_all();
|
||||||
|
|
||||||
|
if has_ready_work() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
current_thread()
|
||||||
|
.shared
|
||||||
|
.closing
|
||||||
|
.store(false, Ordering::Release);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drains already-queued work on the current runtime thread without polling the
|
||||||
|
/// driver for timers or I/O readiness.
|
||||||
|
///
|
||||||
|
/// This is intended for host integrations that need to flush application work
|
||||||
|
/// from inside a host callback without re-entering timer callbacks.
|
||||||
|
pub fn run_ready_tasks() {
|
||||||
|
let _ = current_thread();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
drain_remote_tasks();
|
||||||
|
drain_completed_workers();
|
||||||
|
|
||||||
|
let mut microtasks_run: u64 = 0;
|
||||||
|
while let Some(task) = pop_microtask() {
|
||||||
|
task();
|
||||||
|
microtasks_run += 1;
|
||||||
|
drain_remote_tasks();
|
||||||
|
drain_completed_workers();
|
||||||
|
}
|
||||||
|
if microtasks_run >= MICROTASK_STARVATION_THRESHOLD {
|
||||||
|
tracing::warn!(
|
||||||
|
target: trace_targets::SCHEDULER,
|
||||||
|
event = "microtask_starvation",
|
||||||
|
count = microtasks_run,
|
||||||
|
"microtask queue ran {microtasks_run} tasks in a single turn; macrotask handlers may be starved",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(task) = pop_macrotask() {
|
||||||
|
task();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
drain_remote_tasks();
|
||||||
|
drain_completed_workers();
|
||||||
|
|
||||||
|
if has_ready_work() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
current_thread()
|
||||||
|
.shared
|
||||||
|
.closing
|
||||||
|
.store(false, Ordering::Release);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn drain_all() {
|
fn drain_all() {
|
||||||
drain_driver_events();
|
drain_driver_events();
|
||||||
drain_remote_tasks();
|
drain_remote_tasks();
|
||||||
|
|||||||
@@ -480,6 +480,99 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Drains ready work on the current runtime thread without blocking for future work.
|
||||||
|
///
|
||||||
|
/// Unlike [`run`], this returns as soon as there are no immediately runnable
|
||||||
|
/// microtasks or macrotasks left. It is intended for host integrations that
|
||||||
|
/// need to re-enter the scheduler while an outer platform loop remains active.
|
||||||
|
pub fn run_until_stalled() {
|
||||||
|
let _ = current_thread();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
drain_all();
|
||||||
|
|
||||||
|
let mut microtasks_run: u64 = 0;
|
||||||
|
while let Some(task) = pop_microtask() {
|
||||||
|
task();
|
||||||
|
microtasks_run += 1;
|
||||||
|
drain_all();
|
||||||
|
}
|
||||||
|
if microtasks_run >= MICROTASK_STARVATION_THRESHOLD {
|
||||||
|
tracing::warn!(
|
||||||
|
target: trace_targets::SCHEDULER,
|
||||||
|
event = "microtask_starvation",
|
||||||
|
count = microtasks_run,
|
||||||
|
"microtask queue ran {microtasks_run} tasks in a single turn; macrotask handlers may be starved",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(task) = pop_macrotask() {
|
||||||
|
task();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
drain_all();
|
||||||
|
|
||||||
|
if has_ready_work() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
current_thread()
|
||||||
|
.shared
|
||||||
|
.closing
|
||||||
|
.store(false, Ordering::Release);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drains already-queued work on the current runtime thread without polling the
|
||||||
|
/// driver for timers or I/O readiness.
|
||||||
|
///
|
||||||
|
/// This is intended for host integrations that need to flush application work
|
||||||
|
/// from inside a host callback without re-entering timer callbacks.
|
||||||
|
pub fn run_ready_tasks() {
|
||||||
|
let _ = current_thread();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
drain_remote_tasks();
|
||||||
|
drain_completed_workers();
|
||||||
|
|
||||||
|
let mut microtasks_run: u64 = 0;
|
||||||
|
while let Some(task) = pop_microtask() {
|
||||||
|
task();
|
||||||
|
microtasks_run += 1;
|
||||||
|
drain_remote_tasks();
|
||||||
|
drain_completed_workers();
|
||||||
|
}
|
||||||
|
if microtasks_run >= MICROTASK_STARVATION_THRESHOLD {
|
||||||
|
tracing::warn!(
|
||||||
|
target: trace_targets::SCHEDULER,
|
||||||
|
event = "microtask_starvation",
|
||||||
|
count = microtasks_run,
|
||||||
|
"microtask queue ran {microtasks_run} tasks in a single turn; macrotask handlers may be starved",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(task) = pop_macrotask() {
|
||||||
|
task();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
drain_remote_tasks();
|
||||||
|
drain_completed_workers();
|
||||||
|
|
||||||
|
if has_ready_work() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
current_thread()
|
||||||
|
.shared
|
||||||
|
.closing
|
||||||
|
.store(false, Ordering::Release);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn drain_all() {
|
fn drain_all() {
|
||||||
drain_driver_events();
|
drain_driver_events();
|
||||||
drain_remote_tasks();
|
drain_remote_tasks();
|
||||||
|
|||||||
@@ -270,20 +270,14 @@ fn layout_element(
|
|||||||
&& !rects_overlap(rect, clip)
|
&& !rects_overlap(rect, clip)
|
||||||
{
|
{
|
||||||
perf_stats.viewport_culled += 1;
|
perf_stats.viewport_culled += 1;
|
||||||
return LayoutNode {
|
return layout_interaction_skeleton(
|
||||||
path,
|
element,
|
||||||
element_id: element.id,
|
|
||||||
rect,
|
rect,
|
||||||
corner_radius: 0.0,
|
path,
|
||||||
clip_rect: None,
|
text_system,
|
||||||
pointer_events: element.style.pointer_events,
|
perf_stats,
|
||||||
focusable: element.style.focusable,
|
layout_cache,
|
||||||
cursor: element.style.cursor.unwrap_or(CursorIcon::Default),
|
);
|
||||||
scroll_metrics: None,
|
|
||||||
prepared_image: None,
|
|
||||||
prepared_text: None,
|
|
||||||
children: Vec::new(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Incremental layout cache check.
|
// Incremental layout cache check.
|
||||||
@@ -295,6 +289,7 @@ fn layout_element(
|
|||||||
subtree_hash,
|
subtree_hash,
|
||||||
avail_width_bits: rect.size.width.round() as u32,
|
avail_width_bits: rect.size.width.round() as u32,
|
||||||
avail_height_bits: rect.size.height.round() as u32,
|
avail_height_bits: rect.size.height.round() as u32,
|
||||||
|
clip_rect_bits: cache_clip_rect_bits(rect, clip_rect),
|
||||||
};
|
};
|
||||||
if let Some(cached) = layout_cache.results.get(&cache_key) {
|
if let Some(cached) = layout_cache.results.get(&cache_key) {
|
||||||
let offset = rect.origin;
|
let offset = rect.origin;
|
||||||
@@ -888,6 +883,210 @@ fn layout_container_children(
|
|||||||
children
|
children
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn layout_interaction_skeleton(
|
||||||
|
element: &Element,
|
||||||
|
rect: Rect,
|
||||||
|
path: LayoutPath,
|
||||||
|
text_system: &mut TextSystem,
|
||||||
|
perf_stats: &mut LayoutPerfStats,
|
||||||
|
layout_cache: &mut LayoutCache,
|
||||||
|
) -> LayoutNode {
|
||||||
|
let cursor = element.style.cursor.unwrap_or_else(|| {
|
||||||
|
if element.text_node().is_some() {
|
||||||
|
CursorIcon::Text
|
||||||
|
} else {
|
||||||
|
CursorIcon::Default
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let mut interaction = LayoutNode {
|
||||||
|
path,
|
||||||
|
element_id: element.id,
|
||||||
|
rect,
|
||||||
|
corner_radius: uniform_corner_radius(&element.style, rect),
|
||||||
|
clip_rect: None,
|
||||||
|
pointer_events: element.style.pointer_events,
|
||||||
|
focusable: element.style.focusable,
|
||||||
|
cursor,
|
||||||
|
scroll_metrics: None,
|
||||||
|
prepared_image: None,
|
||||||
|
prepared_text: None,
|
||||||
|
children: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if rect.size.width <= 0.0 || rect.size.height <= 0.0 {
|
||||||
|
return interaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(scroll_box) = element.scroll_box_node() {
|
||||||
|
let content = inset_rect(rect, content_insets(&element.style));
|
||||||
|
if content.size.width <= 0.0 || content.size.height <= 0.0 {
|
||||||
|
return interaction;
|
||||||
|
}
|
||||||
|
let gutter_width = scroll_box
|
||||||
|
.scrollbar
|
||||||
|
.gutter_width
|
||||||
|
.max(0.0)
|
||||||
|
.min(content.size.width);
|
||||||
|
let viewport_rect = Rect::new(
|
||||||
|
content.origin.x,
|
||||||
|
content.origin.y,
|
||||||
|
(content.size.width - gutter_width).max(0.0),
|
||||||
|
content.size.height,
|
||||||
|
);
|
||||||
|
interaction.clip_rect = Some(viewport_rect);
|
||||||
|
let content_size = intrinsic_container_content_size(
|
||||||
|
element,
|
||||||
|
UiSize::new(
|
||||||
|
viewport_rect.size.width.max(0.0),
|
||||||
|
viewport_rect.size.height.max(0.0),
|
||||||
|
),
|
||||||
|
text_system,
|
||||||
|
perf_stats,
|
||||||
|
layout_cache,
|
||||||
|
);
|
||||||
|
let content_height = content_size.height.max(viewport_rect.size.height);
|
||||||
|
let offset_y = scroll_box
|
||||||
|
.offset_y
|
||||||
|
.max(0.0)
|
||||||
|
.min((content_height - viewport_rect.size.height).max(0.0));
|
||||||
|
interaction.children = layout_interaction_skeleton_children(
|
||||||
|
element,
|
||||||
|
Rect::new(
|
||||||
|
viewport_rect.origin.x,
|
||||||
|
viewport_rect.origin.y - offset_y,
|
||||||
|
viewport_rect.size.width,
|
||||||
|
content_height,
|
||||||
|
),
|
||||||
|
&interaction.path,
|
||||||
|
text_system,
|
||||||
|
perf_stats,
|
||||||
|
layout_cache,
|
||||||
|
);
|
||||||
|
let (scrollbar_track, scrollbar_thumb) = scrollbar_geometry(
|
||||||
|
content,
|
||||||
|
scroll_box.scrollbar,
|
||||||
|
content_height,
|
||||||
|
viewport_rect.size.height,
|
||||||
|
offset_y,
|
||||||
|
);
|
||||||
|
interaction.scroll_metrics = Some(ScrollMetrics {
|
||||||
|
viewport_rect,
|
||||||
|
content_height,
|
||||||
|
offset_y,
|
||||||
|
max_offset_y: (content_height - viewport_rect.size.height).max(0.0),
|
||||||
|
scrollbar_track,
|
||||||
|
scrollbar_thumb,
|
||||||
|
});
|
||||||
|
return interaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
if element.text_node().is_some()
|
||||||
|
|| element.image_node().is_some()
|
||||||
|
|| element.children.is_empty()
|
||||||
|
{
|
||||||
|
return interaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = inset_rect(rect, content_insets(&element.style));
|
||||||
|
interaction.children = layout_interaction_skeleton_children(
|
||||||
|
element,
|
||||||
|
content,
|
||||||
|
&interaction.path,
|
||||||
|
text_system,
|
||||||
|
perf_stats,
|
||||||
|
layout_cache,
|
||||||
|
);
|
||||||
|
interaction
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout_interaction_skeleton_children(
|
||||||
|
element: &Element,
|
||||||
|
content: Rect,
|
||||||
|
path: &LayoutPath,
|
||||||
|
text_system: &mut TextSystem,
|
||||||
|
perf_stats: &mut LayoutPerfStats,
|
||||||
|
layout_cache: &mut LayoutCache,
|
||||||
|
) -> Vec<LayoutNode> {
|
||||||
|
if element.children.is_empty() || content.size.width <= 0.0 || content.size.height <= 0.0 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let gap_count = element.children.len().saturating_sub(1) as f32;
|
||||||
|
let total_gap = element.style.gap * gap_count;
|
||||||
|
let available_main =
|
||||||
|
(main_axis_size(content.size, element.style.direction).max(0.0) - total_gap).max(0.0);
|
||||||
|
let available_cross = cross_axis_size(content.size, element.style.direction).max(0.0);
|
||||||
|
|
||||||
|
let mut measured_children = Vec::with_capacity(element.children.len());
|
||||||
|
let mut fixed_total = 0.0;
|
||||||
|
let mut flex_total = 0.0;
|
||||||
|
for child in &element.children {
|
||||||
|
let cross = child_cross_size(child, element.style.direction)
|
||||||
|
.unwrap_or(available_cross)
|
||||||
|
.clamp(0.0, available_cross);
|
||||||
|
let explicit_main =
|
||||||
|
child_main_size(child, element.style.direction).map(|main| main.max(0.0));
|
||||||
|
let is_flex = explicit_main.is_none() && child.style.flex_grow > 0.0;
|
||||||
|
let measured_main = explicit_main.unwrap_or_else(|| {
|
||||||
|
if is_flex {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
intrinsic_main_size(
|
||||||
|
child,
|
||||||
|
element.style.direction,
|
||||||
|
cross,
|
||||||
|
available_main,
|
||||||
|
text_system,
|
||||||
|
perf_stats,
|
||||||
|
layout_cache,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if is_flex {
|
||||||
|
flex_total += child_flex_weight(child);
|
||||||
|
} else {
|
||||||
|
fixed_total += measured_main;
|
||||||
|
}
|
||||||
|
measured_children.push(MeasuredChild {
|
||||||
|
cross,
|
||||||
|
main: measured_main,
|
||||||
|
is_flex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let remaining_main = (available_main - fixed_total).max(0.0);
|
||||||
|
let mut cursor = main_axis_origin(content, element.style.direction);
|
||||||
|
let mut children = Vec::with_capacity(element.children.len());
|
||||||
|
for (index, (child, measured)) in element.children.iter().zip(measured_children).enumerate() {
|
||||||
|
let child_main = if measured.is_flex {
|
||||||
|
if flex_total <= 0.0 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
remaining_main * (child_flex_weight(child) / flex_total)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
measured.main
|
||||||
|
};
|
||||||
|
let child_rect = child_rect(
|
||||||
|
content,
|
||||||
|
element.style.direction,
|
||||||
|
cursor,
|
||||||
|
child_main.max(0.0),
|
||||||
|
measured.cross,
|
||||||
|
);
|
||||||
|
children.push(layout_interaction_skeleton(
|
||||||
|
child,
|
||||||
|
child_rect,
|
||||||
|
path.child(index),
|
||||||
|
text_system,
|
||||||
|
perf_stats,
|
||||||
|
layout_cache,
|
||||||
|
));
|
||||||
|
cursor += child_main.max(0.0) + element.style.gap;
|
||||||
|
}
|
||||||
|
children
|
||||||
|
}
|
||||||
|
|
||||||
fn intrinsic_main_size(
|
fn intrinsic_main_size(
|
||||||
child: &Element,
|
child: &Element,
|
||||||
direction: FlexDirection,
|
direction: FlexDirection,
|
||||||
@@ -1147,6 +1346,7 @@ struct LayoutCacheKey {
|
|||||||
subtree_hash: u64,
|
subtree_hash: u64,
|
||||||
avail_width_bits: u32,
|
avail_width_bits: u32,
|
||||||
avail_height_bits: u32,
|
avail_height_bits: u32,
|
||||||
|
clip_rect_bits: Option<(u32, u32, u32, u32)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CachedLayout {
|
struct CachedLayout {
|
||||||
@@ -1208,6 +1408,16 @@ fn rects_overlap(a: Rect, b: Rect) -> bool {
|
|||||||
&& a.origin.y + a.size.height > b.origin.y
|
&& a.origin.y + a.size.height > b.origin.y
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cache_clip_rect_bits(rect: Rect, clip_rect: Option<Rect>) -> Option<(u32, u32, u32, u32)> {
|
||||||
|
let clip = clip_rect?;
|
||||||
|
Some((
|
||||||
|
(clip.origin.x - rect.origin.x).round().to_bits(),
|
||||||
|
(clip.origin.y - rect.origin.y).round().to_bits(),
|
||||||
|
clip.size.width.round().to_bits(),
|
||||||
|
clip.size.height.round().to_bits(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fn prepare_image(image: &ImageNode, rect: Rect, element_id: Option<ElementId>) -> PreparedImage {
|
fn prepare_image(image: &ImageNode, rect: Rect, element_id: Option<ElementId>) -> PreparedImage {
|
||||||
let source_size = image.resource.size();
|
let source_size = image.resource.size();
|
||||||
let source_aspect = if source_size.height > 0.0 {
|
let source_aspect = if source_size.height > 0.0 {
|
||||||
@@ -1599,7 +1809,9 @@ fn child_rect(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{LayoutCache, layout_scene, layout_snapshot, layout_snapshot_with_cache};
|
use super::{
|
||||||
|
LayoutCache, LayoutSnapshot, layout_scene, layout_snapshot, layout_snapshot_with_cache,
|
||||||
|
};
|
||||||
use crate::scene::{Color, DisplayItem, Point, Quad, Rect, UiSize};
|
use crate::scene::{Color, DisplayItem, Point, Quad, Rect, UiSize};
|
||||||
use crate::text::TextSystem;
|
use crate::text::TextSystem;
|
||||||
use crate::text::{TextStyle, TextWrap};
|
use crate::text::{TextStyle, TextWrap};
|
||||||
@@ -2254,7 +2466,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scroll_culling_skips_off_screen_children() {
|
fn scroll_culling_skips_off_screen_children() {
|
||||||
use crate::tree::ScrollbarStyle;
|
|
||||||
let mut text_system = TextSystem::new();
|
let mut text_system = TextSystem::new();
|
||||||
let mut layout_cache = LayoutCache::new();
|
let mut layout_cache = LayoutCache::new();
|
||||||
|
|
||||||
@@ -2296,4 +2507,108 @@ mod tests {
|
|||||||
"expected >= 4 text items visible, got {text_items}"
|
"expected >= 4 text items visible, got {text_items}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cached_scroll_container_recomputes_visible_children_after_offset_changes() {
|
||||||
|
fn scroll_root(offset_y: f32) -> Element {
|
||||||
|
let mut list = Element::column().gap(0.0);
|
||||||
|
for i in 0..20 {
|
||||||
|
list = list.child(
|
||||||
|
Element::new()
|
||||||
|
.height(40.0)
|
||||||
|
.child(Element::text(format!("item {i:02}"), text_style())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Element::new().child(
|
||||||
|
Element::scroll_box(offset_y)
|
||||||
|
.width(400.0)
|
||||||
|
.height(200.0)
|
||||||
|
.child(list),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visible_texts(snapshot: &LayoutSnapshot) -> Vec<&str> {
|
||||||
|
snapshot
|
||||||
|
.scene
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.filter_map(|item| match item {
|
||||||
|
DisplayItem::Text(text) => Some(text.text.as_str()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut text_system = TextSystem::new();
|
||||||
|
let mut layout_cache = LayoutCache::new();
|
||||||
|
let size = UiSize::new(400.0, 200.0);
|
||||||
|
|
||||||
|
let first = layout_snapshot_with_cache(
|
||||||
|
1,
|
||||||
|
size,
|
||||||
|
&scroll_root(0.0),
|
||||||
|
&mut text_system,
|
||||||
|
&mut layout_cache,
|
||||||
|
);
|
||||||
|
let second = layout_snapshot_with_cache(
|
||||||
|
2,
|
||||||
|
size,
|
||||||
|
&scroll_root(240.0),
|
||||||
|
&mut text_system,
|
||||||
|
&mut layout_cache,
|
||||||
|
);
|
||||||
|
|
||||||
|
let first_visible = visible_texts(&first);
|
||||||
|
let second_visible = visible_texts(&second);
|
||||||
|
assert!(first_visible.iter().any(|text| text.contains("item 00")));
|
||||||
|
assert!(
|
||||||
|
second_visible.iter().any(|text| text.contains("item 06")),
|
||||||
|
"expected scrolled snapshot to include newly visible rows, got {second_visible:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!second_visible.iter().any(|text| text.contains("item 00")),
|
||||||
|
"expected initial rows to be culled after scrolling, got {second_visible:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn culled_container_keeps_descendant_geometry_for_scroll_into_view() {
|
||||||
|
let target_id = ElementId::new(42);
|
||||||
|
let root = Element::new().child(
|
||||||
|
Element::scroll_box(0.0).width(400.0).height(120.0).child(
|
||||||
|
Element::column()
|
||||||
|
.child(
|
||||||
|
Element::column()
|
||||||
|
.height(160.0)
|
||||||
|
.child(Element::text("visible group", text_style())),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Element::column().height(160.0).child(
|
||||||
|
Element::new()
|
||||||
|
.id(target_id)
|
||||||
|
.height(40.0)
|
||||||
|
.child(Element::text("offscreen target", text_style())),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let snapshot = layout_snapshot(1, UiSize::new(400.0, 120.0), &root);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
snapshot
|
||||||
|
.scene
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.all(|item| !matches!(item, DisplayItem::Text(text) if text.text.contains("offscreen target"))),
|
||||||
|
"offscreen target text should still be scene-culled"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
snapshot
|
||||||
|
.interaction_tree
|
||||||
|
.rect_for_element(target_id)
|
||||||
|
.is_some(),
|
||||||
|
"offscreen descendants must keep geometry for widget-ref scrolling"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ pub use layout::{
|
|||||||
pub use layout::{layout_scene, layout_scene_with_text_system};
|
pub use layout::{layout_scene, layout_scene_with_text_system};
|
||||||
pub use platform::{
|
pub use platform::{
|
||||||
PlatformClosed, PlatformEndpoint, PlatformEvent, PlatformProxy, PlatformRequest,
|
PlatformClosed, PlatformEndpoint, PlatformEvent, PlatformProxy, PlatformRequest,
|
||||||
PlatformRuntime, start_headless,
|
PlatformRuntime, set_command_wake_hook, start_headless,
|
||||||
};
|
};
|
||||||
pub use runtime::{EventStreamClosed, UiRuntime, WindowController};
|
pub use runtime::{EventStreamClosed, UiRuntime, WindowController};
|
||||||
pub use scene::{
|
pub use scene::{
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ use std::cell::RefCell;
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
use std::sync::{Arc, Mutex, OnceLock};
|
||||||
|
|
||||||
use ruin_runtime::channel::mpsc;
|
use ruin_runtime::channel::mpsc;
|
||||||
use ruin_runtime::{WorkerHandle, queue_future, queue_microtask, spawn_worker};
|
use ruin_runtime::{WorkerHandle, queue_future, queue_microtask, spawn_worker};
|
||||||
@@ -18,6 +18,13 @@ use crate::trace_targets;
|
|||||||
use crate::tree::CursorIcon;
|
use crate::tree::CursorIcon;
|
||||||
use crate::window::{WindowConfigured, WindowId, WindowLifecycle, WindowSpec, WindowUpdate};
|
use crate::window::{WindowConfigured, WindowId, WindowLifecycle, WindowSpec, WindowUpdate};
|
||||||
|
|
||||||
|
type CommandWakeHook = Arc<dyn Fn() + Send + Sync + 'static>;
|
||||||
|
|
||||||
|
fn command_wake_hook() -> &'static Mutex<Option<CommandWakeHook>> {
|
||||||
|
static HOOK: OnceLock<Mutex<Option<CommandWakeHook>>> = OnceLock::new();
|
||||||
|
HOOK.get_or_init(|| Mutex::new(None))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PlatformProxy {
|
pub struct PlatformProxy {
|
||||||
command_tx: mpsc::UnboundedSender<PlatformRequest>,
|
command_tx: mpsc::UnboundedSender<PlatformRequest>,
|
||||||
@@ -346,7 +353,20 @@ impl PlatformProxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn send(&self, command: PlatformRequest) -> Result<(), PlatformClosed> {
|
fn send(&self, command: PlatformRequest) -> Result<(), PlatformClosed> {
|
||||||
self.command_tx.send(command).map_err(|_| PlatformClosed)
|
self.command_tx.send(command).map_err(|_| PlatformClosed)?;
|
||||||
|
if let Ok(hook) = command_wake_hook().lock()
|
||||||
|
&& let Some(hook) = hook.as_ref().cloned()
|
||||||
|
{
|
||||||
|
hook();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn set_command_wake_hook(hook: Option<CommandWakeHook>) {
|
||||||
|
if let Ok(mut slot) = command_wake_hook().lock() {
|
||||||
|
*slot = hook;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ libc = "0.2"
|
|||||||
objc2 = "0.6.4"
|
objc2 = "0.6.4"
|
||||||
objc2-core-foundation = "0.3.2"
|
objc2-core-foundation = "0.3.2"
|
||||||
objc2-foundation = "0.3.2"
|
objc2-foundation = "0.3.2"
|
||||||
|
objc2-quartz-core = "0.3.2"
|
||||||
raw-window-handle = "0.6"
|
raw-window-handle = "0.6"
|
||||||
|
raw-window-metal = "1.1.0"
|
||||||
ruin_ui = { path = "../ui" }
|
ruin_ui = { path = "../ui" }
|
||||||
ruin_ui_renderer_wgpu = { path = "../ui_renderer_wgpu" }
|
ruin_ui_renderer_wgpu = { path = "../ui_renderer_wgpu" }
|
||||||
ruin_runtime = { package = "ruin-runtime", path = "../runtime" }
|
ruin_runtime = { package = "ruin-runtime", path = "../runtime" }
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -91,7 +91,8 @@ struct PreparedGlyphBitmap {
|
|||||||
|
|
||||||
struct RasterizedText {
|
struct RasterizedText {
|
||||||
origin_offset: Point,
|
origin_offset: Point,
|
||||||
size: UiSize,
|
draw_size: UiSize,
|
||||||
|
texture_size: UiSize,
|
||||||
pixels: Vec<u8>,
|
pixels: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,6 +171,7 @@ struct UploadedAtlasText {
|
|||||||
struct TextTextureKey {
|
struct TextTextureKey {
|
||||||
text: String,
|
text: String,
|
||||||
bounds: Option<(u32, u32)>,
|
bounds: Option<(u32, u32)>,
|
||||||
|
clip: Option<(u32, u32, u32, u32)>,
|
||||||
font_size_bits: u32,
|
font_size_bits: u32,
|
||||||
line_height_bits: u32,
|
line_height_bits: u32,
|
||||||
color: (u8, u8, u8, u8),
|
color: (u8, u8, u8, u8),
|
||||||
@@ -330,6 +332,7 @@ pub struct WgpuSceneRenderer {
|
|||||||
image_cache_order: VecDeque<u64>,
|
image_cache_order: VecDeque<u64>,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
glyph_atlas: GlyphAtlas,
|
glyph_atlas: GlyphAtlas,
|
||||||
|
scale_factor: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_TEXT_CACHE_ENTRIES: usize = 64;
|
const MAX_TEXT_CACHE_ENTRIES: usize = 64;
|
||||||
@@ -366,8 +369,8 @@ impl WgpuSceneRenderer {
|
|||||||
format,
|
format,
|
||||||
width: width.max(1),
|
width: width.max(1),
|
||||||
height: height.max(1),
|
height: height.max(1),
|
||||||
present_mode: wgpu::PresentMode::AutoVsync,
|
present_mode: wgpu::PresentMode::AutoNoVsync,
|
||||||
desired_maximum_frame_latency: 2,
|
desired_maximum_frame_latency: 1,
|
||||||
alpha_mode: caps.alpha_modes[0],
|
alpha_mode: caps.alpha_modes[0],
|
||||||
view_formats: vec![],
|
view_formats: vec![],
|
||||||
};
|
};
|
||||||
@@ -710,6 +713,7 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
image_cache: HashMap::new(),
|
image_cache: HashMap::new(),
|
||||||
image_cache_order: VecDeque::new(),
|
image_cache_order: VecDeque::new(),
|
||||||
glyph_atlas,
|
glyph_atlas,
|
||||||
|
scale_factor: 1.0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -723,9 +727,33 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
self.surface.configure(&self.device, &self.config);
|
self.surface.configure(&self.device, &self.config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_scale_factor(&mut self, scale_factor: f32) {
|
||||||
|
let next = scale_factor.max(1.0);
|
||||||
|
if (self.scale_factor - next).abs() <= f32::EPSILON {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.scale_factor = next;
|
||||||
|
// Text cache is scale-dependent; flush on scale transitions (e.g. display move).
|
||||||
|
self.text_cache.clear();
|
||||||
|
self.text_cache_order.clear();
|
||||||
|
self.glyph_atlas = create_glyph_atlas(
|
||||||
|
&self.device,
|
||||||
|
&self.text_bind_group_layout,
|
||||||
|
&self.text_sampler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render(&mut self, scene: &SceneSnapshot) -> Result<(), RenderError> {
|
pub fn render(&mut self, scene: &SceneSnapshot) -> Result<(), RenderError> {
|
||||||
|
self.render_with_logical_size(scene, scene.logical_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_with_logical_size(
|
||||||
|
&mut self,
|
||||||
|
scene: &SceneSnapshot,
|
||||||
|
logical_size: UiSize,
|
||||||
|
) -> Result<(), RenderError> {
|
||||||
let render_start = std::time::Instant::now();
|
let render_start = std::time::Instant::now();
|
||||||
let vertices = build_vertices(scene);
|
let vertices = build_vertices(scene, logical_size);
|
||||||
let frame = match self.surface.get_current_texture() {
|
let frame = match self.surface.get_current_texture() {
|
||||||
wgpu::CurrentSurfaceTexture::Success(frame)
|
wgpu::CurrentSurfaceTexture::Success(frame)
|
||||||
| wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
|
| wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
|
||||||
@@ -749,7 +777,8 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
let mut uploaded_images = Vec::new();
|
let mut uploaded_images = Vec::new();
|
||||||
let mut uploaded_texts = Vec::new();
|
let mut uploaded_texts = Vec::new();
|
||||||
let mut atlas_perf = AtlasTextPerfStats::default();
|
let mut atlas_perf = AtlasTextPerfStats::default();
|
||||||
let uploaded_atlas_text = self.prepare_uploaded_atlas_text(scene, &mut atlas_perf);
|
let uploaded_atlas_text =
|
||||||
|
self.prepare_uploaded_atlas_text(scene, logical_size, &mut atlas_perf);
|
||||||
let mut clip_stack = Vec::new();
|
let mut clip_stack = Vec::new();
|
||||||
let mut active_clip = ActiveClip::default();
|
let mut active_clip = ActiveClip::default();
|
||||||
for item in &scene.items {
|
for item in &scene.items {
|
||||||
@@ -760,17 +789,19 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
DisplayItem::PopClip => pop_clip_state(&mut clip_stack, &mut active_clip),
|
DisplayItem::PopClip => pop_clip_state(&mut clip_stack, &mut active_clip),
|
||||||
DisplayItem::Image(image) => {
|
DisplayItem::Image(image) => {
|
||||||
if let Some(uploaded) =
|
if let Some(uploaded) =
|
||||||
self.prepare_uploaded_image(image, scene.logical_size, active_clip)
|
self.prepare_uploaded_image(image, logical_size, active_clip)
|
||||||
{
|
{
|
||||||
uploaded_images.push(uploaded);
|
uploaded_images.push(uploaded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DisplayItem::Text(text) => {
|
DisplayItem::Text(text) => {
|
||||||
if text.glyphs.iter().all(|glyph| glyph.cache_key.is_some()) {
|
if (self.scale_factor - 1.0).abs() <= f32::EPSILON
|
||||||
|
&& text.glyphs.iter().all(|glyph| glyph.cache_key.is_some())
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(uploaded) =
|
if let Some(uploaded) =
|
||||||
self.prepare_uploaded_text(text, scene.logical_size, active_clip)
|
self.prepare_uploaded_text(text, logical_size, active_clip)
|
||||||
{
|
{
|
||||||
uploaded_texts.push(uploaded);
|
uploaded_texts.push(uploaded);
|
||||||
}
|
}
|
||||||
@@ -854,21 +885,29 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rasterize_text(&mut self, text: &PreparedText) -> Option<RasterizedText> {
|
fn rasterize_text(
|
||||||
if text.glyphs.iter().all(|glyph| glyph.cache_key.is_some()) {
|
&mut self,
|
||||||
return self.rasterize_prepared_text(text);
|
text: &PreparedText,
|
||||||
|
raster_clip: Option<Rect>,
|
||||||
|
) -> Option<RasterizedText> {
|
||||||
|
if (self.scale_factor - 1.0).abs() <= f32::EPSILON
|
||||||
|
&& text.glyphs.iter().all(|glyph| glyph.cache_key.is_some())
|
||||||
|
{
|
||||||
|
return self.rasterize_prepared_text(text, raster_clip);
|
||||||
}
|
}
|
||||||
|
|
||||||
let glyphs = self.collect_glyph_bitmaps(text);
|
let clip = raster_clip.map(|clip| scale_rect_to_pixel_rect(clip, self.scale_factor));
|
||||||
let clip = match text.bounds {
|
let glyphs = self.collect_glyph_bitmaps(text, clip);
|
||||||
Some(bounds) => PixelRect {
|
let clip = clip
|
||||||
left: 0,
|
.or_else(|| {
|
||||||
top: 0,
|
text.bounds.map(|bounds| PixelRect {
|
||||||
right: bounds.width.ceil() as i32,
|
left: 0,
|
||||||
bottom: bounds.height.ceil() as i32,
|
top: 0,
|
||||||
},
|
right: (bounds.width * self.scale_factor).ceil() as i32,
|
||||||
None => glyph_union(&glyphs)?,
|
bottom: (bounds.height * self.scale_factor).ceil() as i32,
|
||||||
};
|
})
|
||||||
|
})
|
||||||
|
.or_else(|| glyph_union(&glyphs))?;
|
||||||
if clip.width() == 0 || clip.height() == 0 {
|
if clip.width() == 0 || clip.height() == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -879,23 +918,36 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Some(RasterizedText {
|
Some(RasterizedText {
|
||||||
origin_offset: Point::new(clip.left as f32, clip.top as f32),
|
origin_offset: Point::new(
|
||||||
size: UiSize::new(clip.width() as f32, clip.height() as f32),
|
clip.left as f32 / self.scale_factor,
|
||||||
|
clip.top as f32 / self.scale_factor,
|
||||||
|
),
|
||||||
|
draw_size: UiSize::new(
|
||||||
|
clip.width() as f32 / self.scale_factor,
|
||||||
|
clip.height() as f32 / self.scale_factor,
|
||||||
|
),
|
||||||
|
texture_size: UiSize::new(clip.width() as f32, clip.height() as f32),
|
||||||
pixels,
|
pixels,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rasterize_prepared_text(&mut self, text: &PreparedText) -> Option<RasterizedText> {
|
fn rasterize_prepared_text(
|
||||||
|
&mut self,
|
||||||
|
text: &PreparedText,
|
||||||
|
raster_clip: Option<Rect>,
|
||||||
|
) -> Option<RasterizedText> {
|
||||||
let glyphs = self.collect_prepared_glyphs(text);
|
let glyphs = self.collect_prepared_glyphs(text);
|
||||||
let clip = match text.bounds {
|
let clip = raster_clip
|
||||||
Some(bounds) => PixelRect {
|
.map(rect_to_pixel_rect)
|
||||||
left: 0,
|
.or_else(|| {
|
||||||
top: 0,
|
text.bounds.map(|bounds| PixelRect {
|
||||||
right: bounds.width.ceil() as i32,
|
left: 0,
|
||||||
bottom: bounds.height.ceil() as i32,
|
top: 0,
|
||||||
},
|
right: bounds.width.ceil() as i32,
|
||||||
None => glyph_union_prepared(&glyphs)?,
|
bottom: bounds.height.ceil() as i32,
|
||||||
};
|
})
|
||||||
|
})
|
||||||
|
.or_else(|| glyph_union_prepared(&glyphs))?;
|
||||||
if clip.width() == 0 || clip.height() == 0 {
|
if clip.width() == 0 || clip.height() == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -914,7 +966,8 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
|
|
||||||
Some(RasterizedText {
|
Some(RasterizedText {
|
||||||
origin_offset: Point::new(clip.left as f32, clip.top as f32),
|
origin_offset: Point::new(clip.left as f32, clip.top as f32),
|
||||||
size: UiSize::new(clip.width() as f32, clip.height() as f32),
|
draw_size: UiSize::new(clip.width() as f32, clip.height() as f32),
|
||||||
|
texture_size: UiSize::new(clip.width() as f32, clip.height() as f32),
|
||||||
pixels,
|
pixels,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -955,13 +1008,20 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
glyphs
|
glyphs
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_glyph_bitmaps(&mut self, text: &PreparedText) -> Vec<GlyphBitmap> {
|
fn collect_glyph_bitmaps(
|
||||||
let mut buffer = Buffer::new_empty(Metrics::new(text.font_size, text.line_height));
|
&mut self,
|
||||||
|
text: &PreparedText,
|
||||||
|
raster_clip: Option<PixelRect>,
|
||||||
|
) -> Vec<GlyphBitmap> {
|
||||||
|
let mut buffer = Buffer::new_empty(Metrics::new(
|
||||||
|
text.font_size * self.scale_factor,
|
||||||
|
text.line_height * self.scale_factor,
|
||||||
|
));
|
||||||
{
|
{
|
||||||
let mut borrowed = buffer.borrow_with(&mut self.font_system);
|
let mut borrowed = buffer.borrow_with(&mut self.font_system);
|
||||||
borrowed.set_size(
|
borrowed.set_size(
|
||||||
text.bounds.map(|bounds| bounds.width),
|
text.bounds.map(|bounds| bounds.width * self.scale_factor),
|
||||||
text.bounds.map(|bounds| bounds.height),
|
text.bounds.map(|bounds| bounds.height * self.scale_factor),
|
||||||
);
|
);
|
||||||
borrowed.set_text(&text.text, &Attrs::new(), Shaping::Advanced, None);
|
borrowed.set_text(&text.text, &Attrs::new(), Shaping::Advanced, None);
|
||||||
}
|
}
|
||||||
@@ -982,13 +1042,17 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
if width == 0 || height == 0 {
|
if width == 0 || height == 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let rect = PixelRect {
|
||||||
|
left: physical.x + image.placement.left,
|
||||||
|
top: physical.y - image.placement.top,
|
||||||
|
right: physical.x + image.placement.left + width,
|
||||||
|
bottom: physical.y - image.placement.top + height,
|
||||||
|
};
|
||||||
|
if raster_clip.is_some_and(|clip| !pixel_rects_intersect(rect, clip)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
glyphs.push(GlyphBitmap {
|
glyphs.push(GlyphBitmap {
|
||||||
rect: PixelRect {
|
rect,
|
||||||
left: physical.x + image.placement.left,
|
|
||||||
top: physical.y - image.placement.top,
|
|
||||||
right: physical.x + image.placement.left + width,
|
|
||||||
bottom: physical.y - image.placement.top + height,
|
|
||||||
},
|
|
||||||
content: image.content,
|
content: image.content,
|
||||||
color: glyph
|
color: glyph
|
||||||
.color_opt
|
.color_opt
|
||||||
@@ -1003,8 +1067,12 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
fn prepare_uploaded_atlas_text(
|
fn prepare_uploaded_atlas_text(
|
||||||
&mut self,
|
&mut self,
|
||||||
scene: &SceneSnapshot,
|
scene: &SceneSnapshot,
|
||||||
|
logical_size: UiSize,
|
||||||
perf: &mut AtlasTextPerfStats,
|
perf: &mut AtlasTextPerfStats,
|
||||||
) -> Option<UploadedAtlasText> {
|
) -> Option<UploadedAtlasText> {
|
||||||
|
if (self.scale_factor - 1.0).abs() > f32::EPSILON {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let mut vertices = Vec::new();
|
let mut vertices = Vec::new();
|
||||||
let mut clip_stack = Vec::new();
|
let mut clip_stack = Vec::new();
|
||||||
let mut active_clip = ActiveClip::default();
|
let mut active_clip = ActiveClip::default();
|
||||||
@@ -1073,7 +1141,7 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
glyph_rect,
|
glyph_rect,
|
||||||
atlas_glyph.atlas_rect,
|
atlas_glyph.atlas_rect,
|
||||||
clip_rect,
|
clip_rect,
|
||||||
scene.logical_size,
|
logical_size,
|
||||||
resolved_color,
|
resolved_color,
|
||||||
active_clip,
|
active_clip,
|
||||||
);
|
);
|
||||||
@@ -1246,9 +1314,10 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
logical_size: UiSize,
|
logical_size: UiSize,
|
||||||
clip: ActiveClip,
|
clip: ActiveClip,
|
||||||
) -> Option<UploadedText> {
|
) -> Option<UploadedText> {
|
||||||
let key = text_texture_key(text);
|
let raster_clip = text_raster_clip(text, clip, logical_size)?;
|
||||||
|
let key = text_texture_key(text, raster_clip);
|
||||||
if !self.text_cache.contains_key(&key) {
|
if !self.text_cache.contains_key(&key) {
|
||||||
let rasterized = self.rasterize_text(text)?;
|
let rasterized = self.rasterize_text(text, raster_clip)?;
|
||||||
let cached = self.create_cached_text_texture(&rasterized);
|
let cached = self.create_cached_text_texture(&rasterized);
|
||||||
self.text_cache.insert(key.clone(), cached);
|
self.text_cache.insert(key.clone(), cached);
|
||||||
}
|
}
|
||||||
@@ -1316,8 +1385,8 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
|
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
|
||||||
label: Some("ruin-ui-renderer-wgpu-texture"),
|
label: Some("ruin-ui-renderer-wgpu-texture"),
|
||||||
size: wgpu::Extent3d {
|
size: wgpu::Extent3d {
|
||||||
width: text.size.width as u32,
|
width: text.texture_size.width as u32,
|
||||||
height: text.size.height as u32,
|
height: text.texture_size.height as u32,
|
||||||
depth_or_array_layers: 1,
|
depth_or_array_layers: 1,
|
||||||
},
|
},
|
||||||
mip_level_count: 1,
|
mip_level_count: 1,
|
||||||
@@ -1337,12 +1406,12 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
&text.pixels,
|
&text.pixels,
|
||||||
wgpu::TexelCopyBufferLayout {
|
wgpu::TexelCopyBufferLayout {
|
||||||
offset: 0,
|
offset: 0,
|
||||||
bytes_per_row: Some(text.size.width as u32 * 4),
|
bytes_per_row: Some(text.texture_size.width as u32 * 4),
|
||||||
rows_per_image: Some(text.size.height as u32),
|
rows_per_image: Some(text.texture_size.height as u32),
|
||||||
},
|
},
|
||||||
wgpu::Extent3d {
|
wgpu::Extent3d {
|
||||||
width: text.size.width as u32,
|
width: text.texture_size.width as u32,
|
||||||
height: text.size.height as u32,
|
height: text.texture_size.height as u32,
|
||||||
depth_or_array_layers: 1,
|
depth_or_array_layers: 1,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -1365,7 +1434,7 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
|||||||
_texture: texture,
|
_texture: texture,
|
||||||
bind_group,
|
bind_group,
|
||||||
origin_offset: text.origin_offset,
|
origin_offset: text.origin_offset,
|
||||||
size: text.size,
|
size: text.draw_size,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1885,9 +1954,9 @@ fn color_to_f32(color: Color) -> [f32; 4] {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_vertices(scene: &SceneSnapshot) -> Vec<Vertex> {
|
fn build_vertices(scene: &SceneSnapshot, logical_size: UiSize) -> Vec<Vertex> {
|
||||||
let width = scene.logical_size.width.max(1.0);
|
let width = logical_size.width.max(1.0);
|
||||||
let height = scene.logical_size.height.max(1.0);
|
let height = logical_size.height.max(1.0);
|
||||||
let mut vertices = Vec::new();
|
let mut vertices = Vec::new();
|
||||||
let mut clip_stack = Vec::new();
|
let mut clip_stack = Vec::new();
|
||||||
let mut active_clip = ActiveClip::default();
|
let mut active_clip = ActiveClip::default();
|
||||||
@@ -2149,6 +2218,22 @@ fn rect_to_pixel_rect(rect: Rect) -> PixelRect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn scale_rect_to_pixel_rect(rect: Rect, scale: f32) -> PixelRect {
|
||||||
|
PixelRect {
|
||||||
|
left: (rect.origin.x * scale).floor() as i32,
|
||||||
|
top: (rect.origin.y * scale).floor() as i32,
|
||||||
|
right: ((rect.origin.x + rect.size.width) * scale).ceil() as i32,
|
||||||
|
bottom: ((rect.origin.y + rect.size.height) * scale).ceil() as i32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pixel_rects_intersect(first: PixelRect, second: PixelRect) -> bool {
|
||||||
|
first.left < second.right
|
||||||
|
&& first.right > second.left
|
||||||
|
&& first.top < second.bottom
|
||||||
|
&& first.bottom > second.top
|
||||||
|
}
|
||||||
|
|
||||||
fn shadow_blur_extent(blur: f32) -> f32 {
|
fn shadow_blur_extent(blur: f32) -> f32 {
|
||||||
blur.max(0.0) * 2.0
|
blur.max(0.0) * 2.0
|
||||||
}
|
}
|
||||||
@@ -2208,6 +2293,33 @@ fn intersect_rects(first: Option<Rect>, second: Option<Rect>) -> Option<Rect> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn text_raster_clip(
|
||||||
|
text: &PreparedText,
|
||||||
|
clip: ActiveClip,
|
||||||
|
logical_size: UiSize,
|
||||||
|
) -> Option<Option<Rect>> {
|
||||||
|
let mut absolute_clip = Some(Rect::new(0.0, 0.0, logical_size.width, logical_size.height));
|
||||||
|
if clip.rect_active {
|
||||||
|
absolute_clip = intersect_rects(absolute_clip, Some(clip.resolved_rect()?));
|
||||||
|
}
|
||||||
|
if let Some(bounds) = text.bounds {
|
||||||
|
let text_bounds = Rect::new(text.origin.x, text.origin.y, bounds.width, bounds.height);
|
||||||
|
absolute_clip = intersect_rects(absolute_clip, Some(text_bounds));
|
||||||
|
}
|
||||||
|
if absolute_clip.is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(absolute_clip.map(|rect| {
|
||||||
|
Rect::new(
|
||||||
|
rect.origin.x - text.origin.x,
|
||||||
|
rect.origin.y - text.origin.y,
|
||||||
|
rect.size.width,
|
||||||
|
rect.size.height,
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
fn empty_clip_rect() -> Rect {
|
fn empty_clip_rect() -> Rect {
|
||||||
Rect::new(1.0, 1.0, -1.0, -1.0)
|
Rect::new(1.0, 1.0, -1.0, -1.0)
|
||||||
}
|
}
|
||||||
@@ -2330,12 +2442,20 @@ fn clip_visible_glyphs(text: &PreparedText, clip: Option<Rect>) -> &[GlyphInstan
|
|||||||
&text.glyphs[glyph_start..glyph_end.min(text.glyphs.len())]
|
&text.glyphs[glyph_start..glyph_end.min(text.glyphs.len())]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn text_texture_key(text: &PreparedText) -> TextTextureKey {
|
fn text_texture_key(text: &PreparedText, raster_clip: Option<Rect>) -> TextTextureKey {
|
||||||
TextTextureKey {
|
TextTextureKey {
|
||||||
text: text.text.clone(),
|
text: text.text.clone(),
|
||||||
bounds: text
|
bounds: text
|
||||||
.bounds
|
.bounds
|
||||||
.map(|bounds| (bounds.width.to_bits(), bounds.height.to_bits())),
|
.map(|bounds| (bounds.width.to_bits(), bounds.height.to_bits())),
|
||||||
|
clip: raster_clip.map(|clip| {
|
||||||
|
(
|
||||||
|
clip.origin.x.to_bits(),
|
||||||
|
clip.origin.y.to_bits(),
|
||||||
|
clip.size.width.to_bits(),
|
||||||
|
clip.size.height.to_bits(),
|
||||||
|
)
|
||||||
|
}),
|
||||||
font_size_bits: text.font_size.to_bits(),
|
font_size_bits: text.font_size.to_bits(),
|
||||||
line_height_bits: text.line_height.to_bits(),
|
line_height_bits: text.line_height.to_bits(),
|
||||||
color: (
|
color: (
|
||||||
@@ -2365,10 +2485,7 @@ mod tests {
|
|||||||
ActiveClip, blend_rgba, build_text_vertices, build_vertices, clip_rect_array,
|
ActiveClip, blend_rgba, build_text_vertices, build_vertices, clip_rect_array,
|
||||||
push_clip_state, rounded_clip_arrays, text_texture_key,
|
push_clip_state, rounded_clip_arrays, text_texture_key,
|
||||||
};
|
};
|
||||||
use ruin_ui::{
|
use ruin_ui::{ClipRegion, Color, Point, PreparedText, Rect, SceneSnapshot, UiSize};
|
||||||
ClipRegion, Color, GlyphInstance, Point, PreparedText, Rect, SceneSnapshot,
|
|
||||||
TextSelectionStyle, UiSize,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quad_scenes_expand_to_six_vertices_per_quad() {
|
fn quad_scenes_expand_to_six_vertices_per_quad() {
|
||||||
@@ -2389,7 +2506,7 @@ mod tests {
|
|||||||
Color::rgb(0xFF, 0xFF, 0xFF),
|
Color::rgb(0xFF, 0xFF, 0xFF),
|
||||||
));
|
));
|
||||||
|
|
||||||
let vertices = build_vertices(&scene);
|
let vertices = build_vertices(&scene, scene.logical_size);
|
||||||
assert_eq!(vertices.len(), 12);
|
assert_eq!(vertices.len(), 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2421,7 +2538,10 @@ mod tests {
|
|||||||
Color::rgb(0xEE, 0xEE, 0xEE),
|
Color::rgb(0xEE, 0xEE, 0xEE),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(text_texture_key(&first), text_texture_key(&second));
|
assert_eq!(
|
||||||
|
text_texture_key(&first, None),
|
||||||
|
text_texture_key(&second, None)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user