feat: Add fullscreen button

This commit is contained in:
2026-09-15 20:28:51 +02:00
parent 7f4f31a09f
commit 311b797724
+126 -15
View File
@@ -143,6 +143,12 @@ pub struct FractalApp {
/// Distance-estimation shading: darkens toward the set boundary using the /// Distance-estimation shading: darkens toward the set boundary using the
/// orbit derivative, giving crisp filaments at deep zoom instead of speckle. /// orbit derivative, giving crisp filaments at deep zoom instead of speckle.
de_coloring: bool, de_coloring: bool,
/// Whether the controls side panel is expanded. Collapsible so the fractal
/// can take (nearly) the whole screen — important on a phone.
controls_open: bool,
/// Whether the app is in fullscreen (browser Fullscreen API on web, viewport
/// fullscreen on native). Kept in sync with the real state each frame.
fullscreen: bool,
/// Reference orbit (`Z_n` as f32 pairs) for the current view. /// Reference orbit (`Z_n` as f32 pairs) for the current view.
reference: Arc<Vec<[f32; 2]>>, reference: Arc<Vec<[f32; 2]>>,
@@ -243,6 +249,8 @@ impl FractalApp {
palette: 0, palette: 0,
antialias: false, antialias: false,
de_coloring: false, de_coloring: false,
controls_open: true,
fullscreen: false,
reference: Arc::new(Vec::new()), reference: Arc::new(Vec::new()),
generation: 0, generation: 0,
ref_center_re, ref_center_re,
@@ -357,9 +365,10 @@ impl FractalApp {
/// precision the zoom needs), half-height, and a fitting iteration count. /// precision the zoom needs), half-height, and a fitting iteration count.
fn go_to_place(&mut self, re: &str, im: &str, half_height: f64, iterations: u32) { fn go_to_place(&mut self, re: &str, im: &str, half_height: f64, iterations: u32) {
let bits = precision_for(half_height); let bits = precision_for(half_height);
if let (Some(cre), Some(cim)) = if let (Some(cre), Some(cim)) = (
(big_from_decimal_str(re, bits), big_from_decimal_str(im, bits)) big_from_decimal_str(re, bits),
{ big_from_decimal_str(im, bits),
) {
self.mode = FractalMode::Mandelbrot; self.mode = FractalMode::Mandelbrot;
self.view = ViewState::with_center(cre, cim, half_height); self.view = ViewState::with_center(cre, cim, half_height);
// Presets carry a hand-tuned count; don't let the auto-scaler clobber it. // Presets carry a hand-tuned count; don't let the auto-scaler clobber it.
@@ -686,9 +695,11 @@ impl FractalApp {
// Wait for the copy, then read the mapped bytes. // Wait for the copy, then read the mapped bytes.
let (tx, rx) = std::sync::mpsc::channel(); let (tx, rx) = std::sync::mpsc::channel();
er.readback().slice(..).map_async(wgpu::MapMode::Read, move |res| { er.readback()
let _ = tx.send(res); .slice(..)
}); .map_async(wgpu::MapMode::Read, move |res| {
let _ = tx.send(res);
});
let _ = device.poll(wgpu::PollType::Wait { let _ = device.poll(wgpu::PollType::Wait {
submission_index: None, submission_index: None,
timeout: None, timeout: None,
@@ -751,9 +762,11 @@ impl FractalApp {
er.copy_to_readback(&device, &queue); er.copy_to_readback(&device, &queue);
let (tx, rx) = futures_channel::oneshot::channel(); let (tx, rx) = futures_channel::oneshot::channel();
er.readback().slice(..).map_async(wgpu::MapMode::Read, move |res| { er.readback()
let _ = tx.send(res); .slice(..)
}); .map_async(wgpu::MapMode::Read, move |res| {
let _ = tx.send(res);
});
let _ = rx.await; let _ = rx.await;
set_progress(&shared, "Encoding", RENDER_END); set_progress(&shared, "Encoding", RENDER_END);
@@ -801,6 +814,82 @@ impl FractalApp {
} }
} }
/// Floating top-left overlay with the panel toggle and fullscreen toggle.
/// Always on top of the fractal, so both stay reachable when the controls
/// panel is collapsed (the common case on a phone).
fn overlay_buttons(&mut self, ui: &mut egui::Ui) {
egui::Area::new(egui::Id::new("overlay_buttons"))
.anchor(egui::Align2::LEFT_TOP, egui::vec2(8.0, 8.0))
.show(ui.ctx(), |ui| {
egui::Frame::popup(ui.style())
.shadow(egui::Shadow::NONE)
.show(ui, |ui| {
ui.horizontal(|ui| {
let panel_label = if self.controls_open { "Hide" } else { "Menu" };
if ui
.button(panel_label)
.on_hover_text("Show/hide the controls panel")
.clicked()
{
self.controls_open = !self.controls_open;
}
let fs_label = if self.fullscreen {
"Windowed"
} else {
"Fullscreen"
};
if ui
.button(fs_label)
.on_hover_text("Toggle fullscreen")
.clicked()
{
self.fullscreen = !self.fullscreen;
self.apply_fullscreen(ui.ctx());
}
});
});
});
}
/// Push the desired fullscreen state to the platform.
#[cfg(not(target_arch = "wasm32"))]
fn apply_fullscreen(&mut self, ctx: &egui::Context) {
ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(self.fullscreen));
}
/// Push the desired fullscreen state to the browser. `request_fullscreen`
/// must run inside a user gesture; the button click provides the transient
/// activation that carries into this frame.
#[cfg(target_arch = "wasm32")]
fn apply_fullscreen(&mut self, _ctx: &egui::Context) {
let Some(doc) = web_sys::window().and_then(|w| w.document()) else {
return;
};
if self.fullscreen {
if let Some(el) = doc.document_element() {
let _ = el.request_fullscreen();
}
} else {
doc.exit_fullscreen();
}
}
/// Refresh `self.fullscreen` from the real platform state, so the label is
/// correct even when fullscreen is left by Esc/F11 or the browser UI.
#[cfg(not(target_arch = "wasm32"))]
fn sync_fullscreen(&mut self, ctx: &egui::Context) {
if let Some(fs) = ctx.input(|i| i.viewport().fullscreen) {
self.fullscreen = fs;
}
}
#[cfg(target_arch = "wasm32")]
fn sync_fullscreen(&mut self, _ctx: &egui::Context) {
if let Some(doc) = web_sys::window().and_then(|w| w.document()) {
self.fullscreen = doc.fullscreen_element().is_some();
}
}
fn controls_ui(&mut self, ui: &mut egui::Ui) { fn controls_ui(&mut self, ui: &mut egui::Ui) {
ui.heading("Fractal Explorer"); ui.heading("Fractal Explorer");
ui.separator(); ui.separator();
@@ -928,8 +1017,10 @@ impl FractalApp {
.font(egui::TextStyle::Monospace), .font(egui::TextStyle::Monospace),
); );
if re_resp.lost_focus() if re_resp.lost_focus()
&& let Some(v) = && let Some(v) = big_from_decimal_str(
big_from_decimal_str(&self.center_re_edit, parse_bits_for(&self.center_re_edit, bits)) &self.center_re_edit,
parse_bits_for(&self.center_re_edit, bits),
)
{ {
self.view.center_re = v; self.view.center_re = v;
self.view.sync_precision(); self.view.sync_precision();
@@ -945,8 +1036,10 @@ impl FractalApp {
.font(egui::TextStyle::Monospace), .font(egui::TextStyle::Monospace),
); );
if im_resp.lost_focus() if im_resp.lost_focus()
&& let Some(v) = && let Some(v) = big_from_decimal_str(
big_from_decimal_str(&self.center_im_edit, parse_bits_for(&self.center_im_edit, bits)) &self.center_im_edit,
parse_bits_for(&self.center_im_edit, bits),
)
{ {
self.view.center_im = v; self.view.center_im = v;
self.view.sync_precision(); self.view.sync_precision();
@@ -1155,15 +1248,33 @@ impl FractalApp {
impl eframe::App for FractalApp { impl eframe::App for FractalApp {
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
self.poll_export(ui.ctx()); self.poll_export(ui.ctx());
// Track the real fullscreen state (e.g. the user pressing Esc/F11 or the
// browser leaving fullscreen) so the toggle button label stays correct.
self.sync_fullscreen(ui.ctx());
// Cap the panel width so it never swallows a narrow (phone) screen, and
// make it collapsible + scrollable so every parameter stays reachable.
let panel_max = (ui.available_width() * 0.6).clamp(160.0, 340.0);
let mut open = self.controls_open;
egui::Panel::right("controls") egui::Panel::right("controls")
.default_size(280.0) .resizable(true)
.show(ui, |ui| self.controls_ui(ui)); .default_size(panel_max.min(280.0))
.max_size(panel_max)
.show_collapsible(ui, &mut open, |ui| {
egui::ScrollArea::vertical()
.auto_shrink([false, false])
.show(ui, |ui| self.controls_ui(ui));
});
self.controls_open = open;
egui::CentralPanel::default() egui::CentralPanel::default()
.frame(egui::Frame::NONE) .frame(egui::Frame::NONE)
.show(ui, |ui| self.fractal_ui(ui)); .show(ui, |ui| self.fractal_ui(ui));
// Floating overlay, always reachable (even when the panel is collapsed):
// toggle the panel and toggle fullscreen. Essential on a phone.
self.overlay_buttons(ui);
if std::mem::take(&mut self.export_requested) { if std::mem::take(&mut self.export_requested) {
self.do_export(frame); self.do_export(frame);
} }