Fit modal dialogs to the phone screen on mobile

Add mobile::dialog_width(ctx, desired) — a min()-clamp that's a no-op on wide
desktop screens. Clamp the Export and Export Progress modals and the Sample
Import window to it. Render Preferences as an egui::Modal (dim backdrop, centered,
no title bar) with a screen-fit scroll height when mobile_active(); desktop keeps
its window. Extracted the preferences body into render_body() shared by both paths.
This commit is contained in:
Skyler Lehmkuhl 2026-07-01 06:27:59 -04:00
parent a0ac4f2d35
commit f13a127c9d
5 changed files with 102 additions and 64 deletions

View File

@ -208,7 +208,7 @@ impl ExportDialog {
let modal_response = egui::Modal::new(egui::Id::new("export_dialog_modal")) let modal_response = egui::Modal::new(egui::Id::new("export_dialog_modal"))
.show(ctx, |ui| { .show(ctx, |ui| {
ui.set_width(500.0); ui.set_width(crate::mobile::dialog_width(ctx, 500.0));
ui.heading(window_title); ui.heading(window_title);
ui.add_space(8.0); ui.add_space(8.0);
@ -801,7 +801,7 @@ impl ExportProgressDialog {
egui::Modal::new(egui::Id::new("export_progress_modal")) egui::Modal::new(egui::Id::new("export_progress_modal"))
.show(ctx, |ui| { .show(ctx, |ui| {
ui.set_width(400.0); ui.set_width(crate::mobile::dialog_width(ctx, 400.0));
ui.heading("Exporting..."); ui.heading("Exporting...");
ui.add_space(8.0); ui.add_space(8.0);

View File

@ -6329,7 +6329,8 @@ impl eframe::App for EditorApp {
} }
// Render preferences dialog // Render preferences dialog
if let Some(result) = self.preferences_dialog.render(ctx, &mut self.config, &mut self.theme) { let mobile = self.mobile_active();
if let Some(result) = self.preferences_dialog.render(ctx, &mut self.config, &mut self.theme, mobile) {
if result.buffer_size_changed { if result.buffer_size_changed {
println!("⚠️ Audio buffer size will be applied on next app restart"); println!("⚠️ Audio buffer size will be applied on next app restart");
} }

View File

@ -34,6 +34,13 @@ pub const MOBILE_NS: usize = usize::MAX;
const TRANSPORT_H: f32 = 60.0; const TRANSPORT_H: f32 = 60.0;
const TOPBAR_H: f32 = 40.0; const TOPBAR_H: f32 = 40.0;
/// Clamp a desktop dialog width to fit the current screen (with side margins). A no-op on wide
/// desktop screens (`min` keeps the desired width); on a phone-aspect window it shrinks to fit.
pub fn dialog_width(ctx: &egui::Context, desired: f32) -> f32 {
let avail = ctx.screen_rect().width() - 24.0;
desired.min(avail.max(200.0))
}
/// Returns true if the mobile UI is requested via the `LB_MOBILE_UI` env var. /// Returns true if the mobile UI is requested via the `LB_MOBILE_UI` env var.
/// Any non-empty value other than "0" enables it. /// Any non-empty value other than "0" enables it.
pub fn is_mobile_env() -> bool { pub fn is_mobile_env() -> bool {

View File

@ -151,6 +151,7 @@ impl PreferencesDialog {
ctx: &egui::Context, ctx: &egui::Context,
config: &mut AppConfig, config: &mut AppConfig,
theme: &mut Theme, theme: &mut Theme,
mobile: bool,
) -> Option<PreferencesSaveResult> { ) -> Option<PreferencesSaveResult> {
if !self.open { if !self.open {
return None; return None;
@ -160,68 +161,32 @@ impl PreferencesDialog {
let mut should_cancel = false; let mut should_cancel = false;
let mut open = self.open; let mut open = self.open;
egui::Window::new("Preferences") // On mobile, render as a screen-fitting modal sheet (dim backdrop, centered) like the other
.open(&mut open) // mobile modals; on desktop, the familiar draggable window.
.resizable(false) let width = crate::mobile::dialog_width(ctx, 550.0);
.collapsible(false) let scroll_h = if mobile {
.anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO) (ctx.screen_rect().height() - 220.0).clamp(160.0, 400.0)
.show(ctx, |ui| { } else {
ui.set_width(550.0); 400.0
};
// Error message if mobile {
if let Some(error) = &self.error_message { let resp = egui::Modal::new(egui::Id::new("preferences_modal")).show(ctx, |ui| {
ui.colored_label(egui::Color32::from_rgb(255, 100, 100), error); self.render_body(ui, width, scroll_h, &mut should_save, &mut should_cancel);
ui.add_space(8.0);
}
// Tab bar
ui.horizontal(|ui| {
ui.selectable_value(&mut self.tab, PreferencesTab::General, "General");
ui.selectable_value(&mut self.tab, PreferencesTab::Shortcuts, "Keyboard Shortcuts");
});
ui.separator();
// Tab content
match self.tab {
PreferencesTab::General => {
egui::ScrollArea::vertical()
.max_height(400.0)
.show(ui, |ui| {
self.render_general_section(ui);
ui.add_space(8.0);
self.render_audio_section(ui);
ui.add_space(8.0);
self.render_appearance_section(ui);
ui.add_space(8.0);
self.render_startup_section(ui);
ui.add_space(8.0);
self.render_advanced_section(ui);
});
}
PreferencesTab::Shortcuts => {
self.render_shortcuts_tab(ui);
}
}
ui.add_space(16.0);
// Buttons
ui.horizontal(|ui| {
if ui.button("Cancel").clicked() {
should_cancel = true;
}
if ui.button("Reset to Defaults").clicked() {
self.reset_to_defaults();
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("Save").clicked() {
should_save = true;
}
});
});
}); });
if resp.backdrop_response.clicked() {
should_cancel = true;
}
} else {
egui::Window::new("Preferences")
.open(&mut open)
.resizable(false)
.collapsible(false)
.anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
.show(ctx, |ui| {
self.render_body(ui, width, scroll_h, &mut should_save, &mut should_cancel);
});
}
// Update open state // Update open state
self.open = open; self.open = open;
@ -238,6 +203,70 @@ impl PreferencesDialog {
None None
} }
#[allow(clippy::too_many_arguments)]
fn render_body(
&mut self,
ui: &mut egui::Ui,
width: f32,
scroll_h: f32,
should_save: &mut bool,
should_cancel: &mut bool,
) {
ui.set_width(width);
// Error message
if let Some(error) = &self.error_message {
ui.colored_label(egui::Color32::from_rgb(255, 100, 100), error);
ui.add_space(8.0);
}
// Tab bar
ui.horizontal(|ui| {
ui.selectable_value(&mut self.tab, PreferencesTab::General, "General");
ui.selectable_value(&mut self.tab, PreferencesTab::Shortcuts, "Keyboard Shortcuts");
});
ui.separator();
// Tab content
match self.tab {
PreferencesTab::General => {
egui::ScrollArea::vertical().max_height(scroll_h).show(ui, |ui| {
self.render_general_section(ui);
ui.add_space(8.0);
self.render_audio_section(ui);
ui.add_space(8.0);
self.render_appearance_section(ui);
ui.add_space(8.0);
self.render_startup_section(ui);
ui.add_space(8.0);
self.render_advanced_section(ui);
});
}
PreferencesTab::Shortcuts => {
self.render_shortcuts_tab(ui);
}
}
ui.add_space(16.0);
// Buttons
ui.horizontal(|ui| {
if ui.button("Cancel").clicked() {
*should_cancel = true;
}
if ui.button("Reset to Defaults").clicked() {
self.reset_to_defaults();
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("Save").clicked() {
*should_save = true;
}
});
});
}
fn render_shortcuts_tab(&mut self, ui: &mut egui::Ui) { fn render_shortcuts_tab(&mut self, ui: &mut egui::Ui) {
// Capture key events for rebinding BEFORE rendering the rest // Capture key events for rebinding BEFORE rendering the rest
if let Some(rebind_action) = self.rebinding { if let Some(rebind_action) = self.rebinding {

View File

@ -58,7 +58,8 @@ impl SampleImportDialog {
.resizable(true) .resizable(true)
.collapsible(false) .collapsible(false)
.anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO) .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
.default_width(700.0) .default_width(crate::mobile::dialog_width(ctx, 700.0))
.max_width(crate::mobile::dialog_width(ctx, 700.0))
.default_height(500.0) .default_height(500.0)
.show(ctx, |ui| { .show(ctx, |ui| {
// Folder info // Folder info