From f21f48beeacedd904d106edefc045d19d97113ae Mon Sep 17 00:00:00 2001 From: Mehfuz Hossain Date: Mon, 9 Nov 2026 19:36:56 -0800 Subject: [PATCH 1/2] Permission dialog: steps to allow it, styled like the other dialogs Icon, a title per permission (Allow screen recording, Allow camera access), one line on why, and numbered steps naming the System Settings pane. Open System Settings and Not now as app buttons; Escape or a click outside closes it. The cargo run note shows only in development builds. Other errors use the same card. --- apps/studio/src/app.rs | 110 ++++++++++++++++++++++++++---------- apps/studio/src/recorder.rs | 46 +++++++++++++-- 2 files changed, 121 insertions(+), 35 deletions(-) diff --git a/apps/studio/src/app.rs b/apps/studio/src/app.rs index e29c82c..76b5253 100644 --- a/apps/studio/src/app.rs +++ b/apps/studio/src/app.rs @@ -786,6 +786,85 @@ fn ed_name(p: &std::path::Path) -> String { p.file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_default() } +impl App { + /// An error, or the steps to allow a macOS permission, over whatever is on screen. + fn error_modal(&mut self, ctx: &egui::Context, err: &str) { + use crate::widgets::{self, Icon}; + use egui::{vec2, CornerRadius, RichText}; + let pal = crate::theme::p(); + let help = crate::recorder::permission_help(err); + let frame = egui::Frame::NONE.fill(pal.bg).stroke(egui::Stroke::new(1.0, pal.border)).corner_radius(crate::theme::RADIUS).inner_margin(egui::Margin::symmetric(22, 20)).shadow(ctx.global_style().visuals.window_shadow); + // over the floating toolbar the window is see-through: no dimmed backdrop there + let backdrop = if matches!(self.screen, Screen::Picker(_)) { egui::Color32::TRANSPARENT } else { pal.overlay }; + let mut close = false; + let resp = egui::Modal::new(egui::Id::new("error_modal")).frame(frame).backdrop_color(backdrop).show(ctx, |ui| { + ui.set_width(400.0); + ui.spacing_mut().item_spacing = vec2(10.0, 0.0); + let (icon, title) = match &help { + Some(h) => (h.icon, h.title), + None => (Icon::Close, "Something went wrong"), + }; + ui.horizontal(|ui| { + let (tile, _) = ui.allocate_exact_size(vec2(40.0, 40.0), egui::Sense::hover()); + ui.painter().rect_filled(tile, CornerRadius::same(crate::theme::RADIUS_SM), pal.card_2); + widgets::paint_icon(ui.painter(), egui::Rect::from_center_size(tile.center(), vec2(18.0, 18.0)), icon, if help.is_some() { pal.accent } else { pal.danger }); + ui.vertical(|ui| { + ui.add_space(10.0); + ui.label(RichText::new(title).size(16.0).strong().color(pal.text)); + }); + }); + ui.add_space(14.0); + match &help { + Some(h) => { + ui.add(egui::Label::new(RichText::new(h.why).size(13.0).color(pal.text_dim)).wrap()); + ui.add_space(14.0); + let steps = [format!("Open System Settings → Privacy & Security → {}.", h.pane), "Turn on ScreenForge.".to_string(), h.then.to_string()]; + for (i, step) in steps.iter().enumerate() { + ui.horizontal_top(|ui| { + let (dot, _) = ui.allocate_exact_size(vec2(20.0, 20.0), egui::Sense::hover()); + ui.painter().circle_filled(dot.center(), 10.0, pal.card_2); + ui.painter().text(dot.center(), egui::Align2::CENTER_CENTER, (i + 1).to_string(), egui::FontId::proportional(11.5), pal.text); + ui.vertical(|ui| { + ui.add_space(2.0); + ui.add(egui::Label::new(RichText::new(step).size(13.0).color(pal.text)).wrap()); + }); + }); + ui.add_space(8.0); + } + // a development build run from a terminal: macOS asks about the terminal + if cfg!(debug_assertions) { + ui.add(egui::Label::new(RichText::new("Development build: when run with cargo run, allow the terminal app instead.").size(11.5).color(pal.text_faint)).wrap()); + ui.add_space(8.0); + } + ui.add_space(6.0); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if widgets::accent_button(ui, "Open System Settings").clicked() { + let _ = sf_recording::permissions::open_settings(h.permission); + close = true; + } + if widgets::secondary_button(ui, "Not now").clicked() { + close = true; + } + }); + } + None => { + ui.add(egui::Label::new(RichText::new(err).size(13.0).color(pal.text_dim)).wrap()); + ui.add_space(16.0); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if widgets::accent_button(ui, "OK").clicked() { + close = true; + } + }); + } + } + }); + // Escape or a click outside + if close || resp.should_close() { + self.error = None; + } + } +} + impl eframe::App for App { fn on_exit(&mut self) { self.leave_editor(); @@ -1162,36 +1241,7 @@ impl eframe::App for App { } if let Some(err) = self.error.clone() { - let mut open = true; - let help = crate::recorder::permission_help(&err); - let title = if help.is_some() { "Permission needed" } else { "Error" }; - egui::Window::new(title).collapsible(false).resizable(false).open(&mut open).anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]).show(ctx, |ui| { - ui.set_max_width(420.0); - match help { - Some((text, perm)) => { - ui.label(text); - ui.add_space(8.0); - ui.horizontal(|ui| { - if ui.button("Open System Settings").clicked() { - let _ = sf_recording::permissions::open_settings(perm); - self.error = None; - } - if ui.button("Cancel").clicked() { - self.error = None; - } - }); - } - None => { - ui.label(err); - if ui.button("OK").clicked() { - self.error = None; - } - } - } - }); - if !open { - self.error = None; - } + self.error_modal(ctx, &err); } #[cfg(target_os = "linux")] self.update_input_region(ctx); diff --git a/apps/studio/src/recorder.rs b/apps/studio/src/recorder.rs index ab1fb68..a53e7a0 100644 --- a/apps/studio/src/recorder.rs +++ b/apps/studio/src/recorder.rs @@ -81,12 +81,48 @@ pub fn check_permissions(req: &mut RecordRequest) -> Option { None } -/// User-facing text for a `permission:*` message and the settings pane it refers to. -pub fn permission_help(msg: &str) -> Option<(&'static str, sf_recording::Permission)> { +/// What the permission dialog shows for a `permission:*` message. +pub struct PermissionHelp { + pub title: &'static str, + /// Why ScreenForge asks, in a sentence. + pub why: &'static str, + /// The System Settings pane, as the steps name it. + pub pane: &'static str, + /// The last step: what to do once it is allowed. + pub then: &'static str, + pub icon: crate::widgets::Icon, + pub permission: sf_recording::Permission, +} + +/// The dialog for a `permission:*` message (`None` for any other error). +pub fn permission_help(msg: &str) -> Option { + use crate::widgets::Icon; + use sf_recording::Permission; match msg { - "permission:screen" => Some(("ScreenForge needs Screen Recording permission to capture the screen.\n\nEnable ScreenForge in System Settings → Privacy & Security → Screen Recording, then start the recording again. If you launched it from a terminal with cargo run, the permission applies to the terminal app instead.", sf_recording::Permission::ScreenRecording)), - "permission:device" => Some(("ScreenForge needs Camera permission to record an iPhone or iPad: macOS treats a connected device's screen like a camera.\n\nEnable ScreenForge in System Settings → Privacy & Security → Camera, then try again.", sf_recording::Permission::Camera)), - "permission:camera" => Some(("ScreenForge needs Camera permission to record your camera.\n\nEnable ScreenForge in System Settings → Privacy & Security → Camera, or choose \"Don't record camera\" in the toolbar.", sf_recording::Permission::Camera)), + "permission:screen" => Some(PermissionHelp { + title: "Allow screen recording", + why: "macOS needs your OK before ScreenForge can capture the screen.", + pane: "Screen & System Audio Recording", + then: "Quit and reopen ScreenForge if macOS asks, then record again.", + icon: Icon::Display, + permission: Permission::ScreenRecording, + }), + "permission:device" => Some(PermissionHelp { + title: "Allow camera access", + why: "macOS treats a connected iPhone or iPad screen like a camera, so recording one needs camera access.", + pane: "Camera", + then: "Come back and record again.", + icon: Icon::Device, + permission: Permission::Camera, + }), + "permission:camera" => Some(PermissionHelp { + title: "Allow camera access", + why: "macOS needs your OK before ScreenForge can record your camera. Or record without it: choose Don't record camera in the toolbar.", + pane: "Camera", + then: "Come back and record again.", + icon: Icon::Camera, + permission: Permission::Camera, + }), _ => None, } } From 3dabf8f1df12cba8a9bbcd8842c2791dc8758193 Mon Sep 17 00:00:00 2001 From: Mehfuz Hossain Date: Mon, 9 Nov 2026 19:58:32 -0800 Subject: [PATCH 2/2] Chapters icon: a video frame over a bar split into chapters The sidebar's Chapters section showed a filled flag. It now has its own outlined icon, a video frame with a play mark over a segmented progress bar, in the style of the other sidebar icons. Markers on the timeline and in the recording HUD keep the flag. --- apps/studio/src/sidebar.rs | 2 +- apps/studio/src/widgets.rs | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/studio/src/sidebar.rs b/apps/studio/src/sidebar.rs index fa4f990..5223e27 100644 --- a/apps/studio/src/sidebar.rs +++ b/apps/studio/src/sidebar.rs @@ -247,7 +247,7 @@ impl Section { Section::Animations => widgets::Icon::Animations, Section::Agent => widgets::Icon::Agent, Section::DeviceFrame => widgets::Icon::DeviceFrame, - Section::Chapters => widgets::Icon::Flag, + Section::Chapters => widgets::Icon::Chapters, } } diff --git a/apps/studio/src/widgets.rs b/apps/studio/src/widgets.rs index 4e2670e..92600e1 100644 --- a/apps/studio/src/widgets.rs +++ b/apps/studio/src/widgets.rs @@ -330,6 +330,9 @@ pub enum Icon { Pause, Play, Flag, + /// A video frame with a play mark over a progress bar split into chapters (the Chapters + /// section). + Chapters, Restart, /// Magnifier with a plus (a zoom, a bigger preview). Zoom, @@ -586,6 +589,13 @@ pub fn paint_icon(p: &egui::Painter, rect: Rect, icon: Icon, color: Color32) { line((-0.6, -0.8), (-0.6, 0.9)); p.add(egui::Shape::convex_polygon(vec![pos2(c.x - 0.6 * r, c.y - 0.8 * r), pos2(c.x + 0.7 * r, c.y - 0.5 * r), pos2(c.x - 0.6 * r, c.y - 0.1 * r)], color, Stroke::NONE)); } + Icon::Chapters => { + p.rect_stroke(rr(-0.9, -0.8, 0.9, 0.3), 3.0, s, egui::StrokeKind::Middle); + p.add(egui::Shape::convex_polygon(vec![pos2(c.x - 0.18 * r, c.y - 0.5 * r), pos2(c.x + 0.27 * r, c.y - 0.25 * r), pos2(c.x - 0.18 * r, c.y)], color, Stroke::NONE)); + for (a, b) in [(-0.9, -0.38), (-0.18, 0.3), (0.5, 0.9)] { + line((a, 0.75), (b, 0.75)); + } + } Icon::Restart => { // open circular arrow let pts: Vec = (0..=28).map(|i| {