Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 80 additions & 30 deletions apps/studio/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
46 changes: 41 additions & 5 deletions apps/studio/src/recorder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,48 @@ pub fn check_permissions(req: &mut RecordRequest) -> Option<String> {
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<PermissionHelp> {
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,
}
}
Expand Down
2 changes: 1 addition & 1 deletion apps/studio/src/sidebar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand Down
10 changes: 10 additions & 0 deletions apps/studio/src/widgets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<egui::Pos2> = (0..=28).map(|i| {
Expand Down
Loading