Skip to content

Commit

Permalink
Add fullscreen mode to web viewer (#6461)
Browse files Browse the repository at this point in the history
### What

- Closes #6433

This PR enables the `ToggleFullscreen` UI command for web target,
implemented in JS land. The functionality is also exposed via the
`toggle_fullscreen` method. Callbacks are used to communicate about
fullscreen state between WASM and JS.

To test, you'll need to build branch `fullscreen-temp` of
https://github.com/rerun-io/web-viewer-example with a globally linked
`web-viewer` package built from source:

1. Install Yarn: `npm i -g yarn`
2. In this repository:
   - `yarn --cwd rerun_js install`
   - `yarn --cwd rerun_js/web-viewer build`
   - `yarn --cwd rerun_js/web-viewer link`
3. In the `web-viewer-example` repository:
   - `yarn`
   - `yarn link @rerun-io/web-viewer`
   - `yarn dev` + open the page

### Checklist
* [x] I have read and agree to [Contributor
Guide](https://github.com/rerun-io/rerun/blob/main/CONTRIBUTING.md) and
the [Code of
Conduct](https://github.com/rerun-io/rerun/blob/main/CODE_OF_CONDUCT.md)
* [x] I've included a screenshot or gif (if applicable)
* [x] I have tested the web demo (if applicable):
* Using examples from latest `main` build:
[rerun.io/viewer](https://rerun.io/viewer/pr/6461?manifest_url=https://app.rerun.io/version/main/examples_manifest.json)
* Using full set of examples from `nightly` build:
[rerun.io/viewer](https://rerun.io/viewer/pr/6461?manifest_url=https://app.rerun.io/version/nightly/examples_manifest.json)
* [x] The PR title and labels are set such as to maximize their
usefulness for the next release's CHANGELOG
* [x] If applicable, add a new check to the [release
checklist](https://github.com/rerun-io/rerun/blob/main/tests/python/release_checklist)!

- [PR Build Summary](https://build.rerun.io/pr/6461)
- [Recent benchmark results](https://build.rerun.io/graphs/crates.html)
- [Wasm size tracking](https://build.rerun.io/graphs/sizes.html)

To run all checks from `main`, comment on the PR with `@rerun-bot
full-check`.
  • Loading branch information
jprochazk authored May 31, 2024
1 parent 9c571de commit 9ece5dd
Show file tree
Hide file tree
Showing 8 changed files with 301 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ impl PanelState {
self == &Self::Expanded
}

/// Returns `true` if self is [`PanelState::Expanded`]
/// Returns `true` if self is [`PanelState::Hidden`]
#[inline]
pub fn is_hidden(&self) -> bool {
self == &Self::Hidden
Expand Down
12 changes: 11 additions & 1 deletion crates/re_ui/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ pub enum UICommand {
#[cfg(debug_assertions)]
ToggleEguiDebugPanel,

#[cfg(not(target_arch = "wasm32"))]
ToggleFullscreen,
#[cfg(not(target_arch = "wasm32"))]
ZoomIn,
Expand Down Expand Up @@ -171,6 +170,12 @@ impl UICommand {
"Toggle between windowed and fullscreen viewer",
),

#[cfg(target_arch = "wasm32")]
Self::ToggleFullscreen => (
"Toggle fullscreen",
"Toggle between full viewport dimensions and initial dimensions"
),

#[cfg(not(target_arch = "wasm32"))]
Self::ZoomIn => ("Zoom in", "Increases the UI zoom level"),
#[cfg(not(target_arch = "wasm32"))]
Expand Down Expand Up @@ -300,6 +305,8 @@ impl UICommand {

#[cfg(not(target_arch = "wasm32"))]
Self::ToggleFullscreen => Some(key(Key::F11)),
#[cfg(target_arch = "wasm32")]
Self::ToggleFullscreen => None,

#[cfg(not(target_arch = "wasm32"))]
Self::ZoomIn => Some(egui::gui_zoom::kb_shortcuts::ZOOM_IN),
Expand Down Expand Up @@ -336,6 +343,9 @@ impl UICommand {
Self::RestartWithWebGl => None,
#[cfg(target_arch = "wasm32")]
Self::RestartWithWebGpu => None,

#[cfg(target_arch = "wasm32 ")]
Self::ViewportMode(_) => None,
}
}

Expand Down
52 changes: 49 additions & 3 deletions crates/re_viewer/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ pub struct StartupOptions {
/// Forces wgpu backend to use the specified graphics API.
pub force_wgpu_backend: Option<String>,

/// Fullscreen is handled by JS on web.
///
/// This holds some callbacks which we use to communicate
/// about fullscreen state to JS.
#[cfg(target_arch = "wasm32")]
pub fullscreen_options: Option<crate::web::FullscreenOptions>,

/// Default overrides for state of top/side/bottom panels.
pub panel_state_overrides: PanelStateOverrides,
}

Expand All @@ -95,6 +103,9 @@ impl Default for StartupOptions {
expect_data_soon: None,
force_wgpu_backend: None,

#[cfg(target_arch = "wasm32")]
fullscreen_options: Default::default(),

panel_state_overrides: Default::default(),
}
}
Expand Down Expand Up @@ -638,10 +649,8 @@ impl App {
self.egui_debug_panel_open ^= true;
}

#[cfg(not(target_arch = "wasm32"))]
UICommand::ToggleFullscreen => {
let fullscreen = egui_ctx.input(|i| i.viewport().fullscreen.unwrap_or(false));
egui_ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(!fullscreen));
self.toggle_fullscreen();
}

#[cfg(not(target_arch = "wasm32"))]
Expand Down Expand Up @@ -1325,6 +1334,43 @@ impl App {
1.0
}
}

pub(crate) fn toggle_fullscreen(&self) {
#[cfg(not(target_arch = "wasm32"))]
{
let egui_ctx = &self.re_ui.egui_ctx;
let fullscreen = egui_ctx.input(|i| i.viewport().fullscreen.unwrap_or(false));
egui_ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(!fullscreen));
}

#[cfg(target_arch = "wasm32")]
{
if let Some(options) = &self.startup_options.fullscreen_options {
// Tell JS to toggle fullscreen.
if let Err(err) = options.on_toggle.call() {
re_log::error!("{}", crate::web_tools::string_from_js_value(err));
};
}
}
}

#[cfg(target_arch = "wasm32")]
pub(crate) fn is_fullscreen_allowed(&self) -> bool {
self.startup_options.fullscreen_options.is_some()
}

#[cfg(target_arch = "wasm32")]
pub(crate) fn is_fullscreen_mode(&self) -> bool {
if let Some(options) = &self.startup_options.fullscreen_options {
// Ask JS if fullscreen is on or not.
match options.get_state.call() {
Ok(v) => return v.is_truthy(),
Err(err) => re_log::error_once!("{}", crate::web_tools::string_from_js_value(err)),
}
}

false
}
}

#[cfg(target_arch = "wasm32")]
Expand Down
19 changes: 19 additions & 0 deletions crates/re_viewer/src/ui/top_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,25 @@ fn connection_status_ui(ui: &mut egui::Ui, rx: &ReceiveSet<re_log_types::LogMsg>

/// Lay out the panel button right-to-left
fn panel_buttons_r2l(app: &App, app_blueprint: &AppBlueprint<'_>, ui: &mut egui::Ui) {
#[cfg(target_arch = "wasm32")]
if app.is_fullscreen_allowed() {
let mut is_fullscreen = app.is_fullscreen_mode();
let icon = if is_fullscreen {
&re_ui::icons::MINIMIZE
} else {
&re_ui::icons::MAXIMIZE
};

if app
.re_ui()
.medium_icon_toggle_button(ui, icon, &mut is_fullscreen)
.on_hover_text("Toggle fullscreen")
.clicked()
{
app.toggle_fullscreen();
}
}

// selection panel
if app
.re_ui()
Expand Down
20 changes: 19 additions & 1 deletion crates/re_viewer/src/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,17 @@ pub struct AppOptions {
render_backend: Option<String>,
hide_welcome_screen: Option<bool>,
panel_state_overrides: Option<PanelStateOverrides>,
fullscreen: Option<FullscreenOptions>,
}

// Keep in sync with the `FullscreenOptions` typedef in `rerun_js/web-viewer/index.js`
#[derive(Clone, Deserialize)]
pub struct FullscreenOptions {
/// This returns the current fullscreen state, which is a boolean representing on/off.
pub get_state: Callback,

/// This calls the JS version of "toggle fullscreen".
pub on_toggle: Callback,
}

#[derive(Clone, Default, Deserialize)]
Expand All @@ -320,7 +331,13 @@ impl From<PanelStateOverrides> for crate::app_blueprint::PanelStateOverrides {
// Can't deserialize `Option<js_sys::Function>` directly, so newtype it is.
#[derive(Clone, Deserialize)]
#[repr(transparent)]
struct Callback(#[serde(with = "serde_wasm_bindgen::preserve")] js_sys::Function);
pub struct Callback(#[serde(with = "serde_wasm_bindgen::preserve")] js_sys::Function);

impl Callback {
pub fn call(&self) -> Result<JsValue, JsValue> {
self.0.call0(&web_sys::window().unwrap())
}
}

fn create_app(
cc: &eframe::CreationContext<'_>,
Expand All @@ -341,6 +358,7 @@ fn create_app(
expect_data_soon: None,
force_wgpu_backend: None,
hide_welcome_screen: app_options.hide_welcome_screen.unwrap_or(false),
fullscreen_options: app_options.fullscreen.clone(),
panel_state_overrides: app_options.panel_state_overrides.unwrap_or_default().into(),
};
let re_ui = crate::customize_eframe_and_setup_renderer(cc)?;
Expand Down
13 changes: 12 additions & 1 deletion crates/re_viewer/src/web_tools.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{ops::ControlFlow, sync::Arc};

use anyhow::Context as _;
use wasm_bindgen::JsCast as _;
use wasm_bindgen::JsValue;

use re_log::ResultExt as _;
Expand All @@ -11,7 +12,17 @@ use re_viewer_context::CommandSender;
/// Useful in error handlers
#[allow(clippy::needless_pass_by_value)]
pub fn string_from_js_value(s: wasm_bindgen::JsValue) -> String {
s.as_string().unwrap_or(format!("{s:#?}"))
// it's already a string
if let Some(s) = s.as_string() {
return s;
}

// it's an Error, call `toString` instead
if let Some(s) = s.dyn_ref::<js_sys::Error>() {
return format!("{}", s.to_string());
}

format!("{s:#?}")
}

pub fn set_url_parameter_and_refresh(key: &str, value: &str) -> Result<(), wasm_bindgen::JsValue> {
Expand Down
Loading

0 comments on commit 9ece5dd

Please sign in to comment.