249 lines
8.0 KiB
Rust
249 lines
8.0 KiB
Rust
use leptos::prelude::*;
|
|
|
|
use crate::components::layout::{AdminShell, Breadcrumb};
|
|
use crate::terminal::{Renderer, TerminalOptions, TerminalPanel, TerminalRenderEvent};
|
|
|
|
#[component]
|
|
pub fn TerminalPage() -> impl IntoView {
|
|
let renderer = RwSignal::new(initial_terminal_renderer());
|
|
let terminal_options = Signal::derive(move || TerminalOptions {
|
|
renderer: renderer.get(),
|
|
..TerminalOptions::default()
|
|
});
|
|
let on_render = Callback::new(update_terminal_bench_probe);
|
|
|
|
view! {
|
|
<AdminShell>
|
|
<Breadcrumb current="浏览器终端" />
|
|
<section class="cowa-work-surface terminal-page-shell">
|
|
<TerminalRendererSwitch renderer=renderer />
|
|
<TerminalPanel options=terminal_options on_render=on_render />
|
|
</section>
|
|
</AdminShell>
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn TerminalRendererSwitch(renderer: RwSignal<Renderer>) -> impl IntoView {
|
|
view! {
|
|
<div class="terminal-page-toolbar" role="toolbar" aria-label="终端渲染器">
|
|
<span class="terminal-renderer-label">"Renderer"</span>
|
|
<RendererButton renderer=renderer value=Renderer::WebGl label="WebGL" />
|
|
<RendererButton renderer=renderer value=Renderer::Canvas label="Canvas" />
|
|
<RendererButton renderer=renderer value=Renderer::Dom label="DOM" />
|
|
</div>
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn RendererButton(
|
|
renderer: RwSignal<Renderer>,
|
|
value: Renderer,
|
|
label: &'static str,
|
|
) -> impl IntoView {
|
|
view! {
|
|
<button
|
|
type="button"
|
|
class="terminal-renderer-option"
|
|
class=("is-active", move || renderer.get() == value)
|
|
aria-pressed=move || (renderer.get() == value).to_string()
|
|
on:click=move |_| renderer.set(value)
|
|
>
|
|
{label}
|
|
</button>
|
|
}
|
|
}
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
fn initial_terminal_renderer() -> Renderer {
|
|
web_sys::window()
|
|
.and_then(|window| window.location().search().ok())
|
|
.and_then(|query| renderer_from_query(&query))
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
fn initial_terminal_renderer() -> Renderer {
|
|
Renderer::default()
|
|
}
|
|
|
|
#[cfg(any(target_arch = "wasm32", test))]
|
|
fn renderer_from_query(query: &str) -> Option<Renderer> {
|
|
query
|
|
.trim_start_matches('?')
|
|
.split('&')
|
|
.filter_map(|part| part.split_once('='))
|
|
.find_map(|(key, value)| (key == "renderer").then(|| parse_renderer(value)).flatten())
|
|
}
|
|
|
|
#[cfg(any(target_arch = "wasm32", test))]
|
|
fn parse_renderer(value: &str) -> Option<Renderer> {
|
|
match value.to_ascii_lowercase().as_str() {
|
|
"webgl" | "web-gl" => Some(Renderer::WebGl),
|
|
"canvas" | "canvas2d" | "canvas-2d" => Some(Renderer::Canvas),
|
|
"dom" => Some(Renderer::Dom),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
fn update_terminal_bench_probe(event: TerminalRenderEvent) {
|
|
use wasm_bindgen::JsValue;
|
|
|
|
let Some(window) = web_sys::window() else {
|
|
return;
|
|
};
|
|
let Ok(probe) = js_sys::Reflect::get(&window, &JsValue::from_str("__terminalBench")) else {
|
|
return;
|
|
};
|
|
let probe = if probe.is_object() {
|
|
probe
|
|
} else {
|
|
let object = js_sys::Object::new();
|
|
let _ = js_sys::Reflect::set(&window, &JsValue::from_str("__terminalBench"), &object);
|
|
object.into()
|
|
};
|
|
|
|
let count = js_sys::Reflect::get(&probe, &JsValue::from_str("renderCount"))
|
|
.ok()
|
|
.and_then(|value| value.as_f64())
|
|
.unwrap_or(0.0)
|
|
+ 1.0;
|
|
let mode = if event.mode.is_live() {
|
|
"live"
|
|
} else {
|
|
"history"
|
|
};
|
|
let renderer = format!("{:?}", event.renderer);
|
|
let webgl_stats = event.webgl_stats;
|
|
|
|
let _ = js_sys::Reflect::set(
|
|
&probe,
|
|
&JsValue::from_str("renderCount"),
|
|
&JsValue::from_f64(count),
|
|
);
|
|
let _ = js_sys::Reflect::set(
|
|
&probe,
|
|
&JsValue::from_str("lastRenderAt"),
|
|
&JsValue::from_f64(js_sys::Date::now()),
|
|
);
|
|
let _ = js_sys::Reflect::set(
|
|
&probe,
|
|
&JsValue::from_str("renderer"),
|
|
&JsValue::from_str(&renderer),
|
|
);
|
|
let _ = js_sys::Reflect::set(&probe, &JsValue::from_str("mode"), &JsValue::from_str(mode));
|
|
let _ = js_sys::Reflect::set(
|
|
&probe,
|
|
&JsValue::from_str("cols"),
|
|
&JsValue::from_f64(f64::from(event.cols)),
|
|
);
|
|
let _ = js_sys::Reflect::set(
|
|
&probe,
|
|
&JsValue::from_str("rows"),
|
|
&JsValue::from_f64(f64::from(event.rows)),
|
|
);
|
|
let _ = js_sys::Reflect::set(
|
|
&probe,
|
|
&JsValue::from_str("historyRows"),
|
|
&JsValue::from_f64(event.history_rows as f64),
|
|
);
|
|
let _ = js_sys::Reflect::set(
|
|
&probe,
|
|
&JsValue::from_str("screenRows"),
|
|
&JsValue::from_f64(event.screen_rows as f64),
|
|
);
|
|
if let Some(stats) = webgl_stats {
|
|
append_webgl_stats(&probe, stats);
|
|
}
|
|
}
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
fn append_webgl_stats(probe: &wasm_bindgen::JsValue, stats: crate::terminal::WebGlRenderStats) {
|
|
use wasm_bindgen::JsCast;
|
|
use wasm_bindgen::JsValue;
|
|
|
|
let frames = match js_sys::Reflect::get(probe, &JsValue::from_str("webglFrames")) {
|
|
Ok(value) if js_sys::Array::is_array(&value) => value.unchecked_into::<js_sys::Array>(),
|
|
_ => {
|
|
let array = js_sys::Array::new();
|
|
let _ = js_sys::Reflect::set(probe, &JsValue::from_str("webglFrames"), &array);
|
|
array
|
|
}
|
|
};
|
|
|
|
let frame = js_sys::Object::new();
|
|
set_f64(&frame, "totalMs", stats.total_ms);
|
|
set_f64(&frame, "syncDomMs", stats.sync_dom_ms);
|
|
set_f64(&frame, "backdropMs", stats.backdrop_ms);
|
|
set_f64(&frame, "collectMs", stats.collect_ms);
|
|
set_f64(&frame, "backdropTextureMs", stats.backdrop_texture_ms);
|
|
set_f64(&frame, "fallbackCanvasMs", stats.fallback_canvas_ms);
|
|
set_f64(&frame, "atlasMs", stats.atlas_ms);
|
|
set_f64(&frame, "atlasUploadMs", stats.atlas_upload_ms);
|
|
set_f64(&frame, "vertexBuildMs", stats.vertex_build_ms);
|
|
set_f64(&frame, "vertexUploadMs", stats.vertex_upload_ms);
|
|
set_f64(&frame, "drawMs", stats.draw_ms);
|
|
set_f64(&frame, "cells", stats.cells as f64);
|
|
set_f64(&frame, "rowCacheHits", stats.row_cache_hits as f64);
|
|
set_f64(&frame, "rowCacheMisses", stats.row_cache_misses as f64);
|
|
set_f64(&frame, "vertexCacheHits", stats.vertex_cache_hits as f64);
|
|
set_f64(
|
|
&frame,
|
|
"vertexCacheMisses",
|
|
stats.vertex_cache_misses as f64,
|
|
);
|
|
set_f64(&frame, "atlasEntries", stats.atlas_entries as f64);
|
|
set_f64(&frame, "atlasInsertions", stats.atlas_insertions as f64);
|
|
set_f64(&frame, "atlasResets", stats.atlas_resets as f64);
|
|
set_bool(&frame, "fallbackFullCanvas", stats.fallback_full_canvas);
|
|
set_bool(&frame, "glyphDrawFailed", stats.glyph_draw_failed);
|
|
|
|
frames.push(&frame);
|
|
while frames.length() > 720 {
|
|
frames.shift();
|
|
}
|
|
}
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
fn set_f64(target: &js_sys::Object, key: &str, value: f64) {
|
|
use wasm_bindgen::JsValue;
|
|
|
|
let _ = js_sys::Reflect::set(target, &JsValue::from_str(key), &JsValue::from_f64(value));
|
|
}
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
fn set_bool(target: &js_sys::Object, key: &str, value: bool) {
|
|
use wasm_bindgen::JsValue;
|
|
|
|
let _ = js_sys::Reflect::set(target, &JsValue::from_str(key), &JsValue::from_bool(value));
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
fn update_terminal_bench_probe(_event: TerminalRenderEvent) {}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn renderer_query_parses_supported_values() {
|
|
assert_eq!(
|
|
renderer_from_query("?renderer=webgl"),
|
|
Some(Renderer::WebGl)
|
|
);
|
|
assert_eq!(
|
|
renderer_from_query("?foo=1&renderer=canvas2d"),
|
|
Some(Renderer::Canvas)
|
|
);
|
|
assert_eq!(renderer_from_query("?renderer=dom"), Some(Renderer::Dom));
|
|
}
|
|
|
|
#[test]
|
|
fn renderer_query_ignores_unknown_values() {
|
|
assert_eq!(renderer_from_query("?renderer=auto"), None);
|
|
assert_eq!(renderer_from_query("?other=webgl"), None);
|
|
assert_eq!(renderer_from_query(""), None);
|
|
}
|
|
}
|