Media · Capture and devices: recorder, camera, mic meter, device choice and 3D model viewer
ScreenRecorderControls holds a recording in one pill: Record, then a live dot and the time it has run beside pause or resume and stop, with mic and camera toggles. CameraPreview shows host frames in the camera's shape, or the camera off. MicLevelMeter lights segments up to the level, amber and red near the top, with tones shared from data_display. DeviceSelector picks a microphone, camera or speaker through Select, which may now be empty while shut. ModelViewer shows the host's frame for an orbit that a drag, the arrows, Command-scroll, + and - and a reset move; the orbit is seeded from the host so fast drags add up. The gallery draws a stone torus per orbit and frees each replaced frame.
This commit is contained in:
1 parent
cb6c78b90c
commit
d1b8ad395d
20 files changed
+1541
-30
No files matched your search
@@ -0,0 +1,218 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
App, ElementId, ImageSource, IntoElement, ParentElement, RenderOnce, SharedString, Styled,
|
||||
Window, div, img, prelude::*,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
forms::{Choice, OnValue, Select},
|
||||
primitives::{Icon, IconName, framed},
|
||||
theme::{ActiveTheme, IconSize, Radius, TextSize},
|
||||
typography::Ellipsis,
|
||||
};
|
||||
|
||||
/// What a camera sees, in its own shape, from frames the host hands in; the host mirrors them if it will. With no frame it shows the camera off.
|
||||
#[derive(IntoElement)]
|
||||
pub struct CameraPreview {
|
||||
id: ElementId,
|
||||
ratio: f32,
|
||||
frame: Option<ImageSource>,
|
||||
name: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl CameraPreview {
|
||||
/// `ratio` is the camera's width over its height.
|
||||
pub fn new(id: impl Into<ElementId>, ratio: f32) -> Self {
|
||||
assert!(ratio > 0.0, "a camera's shape of {ratio}");
|
||||
Self {
|
||||
id: id.into(),
|
||||
ratio,
|
||||
frame: None,
|
||||
name: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The frame to show now.
|
||||
pub fn frame(mut self, source: impl Into<ImageSource>) -> Self {
|
||||
self.frame = Some(source.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// The camera's name, over the frame's lower corner.
|
||||
pub fn name(mut self, name: impl Into<SharedString>) -> Self {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for CameraPreview {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let round = theme.radius(Radius::Lg);
|
||||
let off = self.frame.is_none();
|
||||
let frame = self.frame.map(|frame| {
|
||||
img(frame)
|
||||
.id((self.id.clone(), "frame"))
|
||||
.size_full()
|
||||
.rounded(round)
|
||||
});
|
||||
let name = self.name.map(|name| {
|
||||
div()
|
||||
.debug_selector(|| "camera-name".into())
|
||||
.absolute()
|
||||
.left_2()
|
||||
.bottom_2()
|
||||
.max_w(gpui::relative(0.8))
|
||||
.px_2()
|
||||
.py_0p5()
|
||||
.rounded(theme.radius(Radius::Sm))
|
||||
.bg(colors.media_backdrop.alpha(0.6))
|
||||
.text_color(colors.on_media)
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.child(Ellipsis::new(name))
|
||||
});
|
||||
framed(self.ratio, cx)
|
||||
.debug_selector(|| "camera-preview".into())
|
||||
.rounded(round)
|
||||
.children(frame)
|
||||
.when(off, |stage| {
|
||||
stage.child(
|
||||
div()
|
||||
.debug_selector(|| "camera-off".into())
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.text_color(colors.fg_muted)
|
||||
.child(
|
||||
Icon::new(IconName::VideoOff)
|
||||
.size(IconSize::Lg)
|
||||
.color(colors.fg_subtle),
|
||||
)
|
||||
.child("Camera off"),
|
||||
)
|
||||
})
|
||||
.children(name)
|
||||
}
|
||||
}
|
||||
|
||||
/// What a device is for.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum DeviceKind {
|
||||
Microphone,
|
||||
Camera,
|
||||
Speaker,
|
||||
}
|
||||
|
||||
impl DeviceKind {
|
||||
fn icon(self) -> IconName {
|
||||
match self {
|
||||
Self::Microphone => IconName::Mic,
|
||||
Self::Camera => IconName::Video,
|
||||
Self::Speaker => IconName::Volume2,
|
||||
}
|
||||
}
|
||||
|
||||
fn noun(self) -> &'static str {
|
||||
match self {
|
||||
Self::Microphone => "microphone",
|
||||
Self::Camera => "camera",
|
||||
Self::Speaker => "speaker",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A device to pick: its key and its name.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Device {
|
||||
pub key: SharedString,
|
||||
pub name: SharedString,
|
||||
}
|
||||
|
||||
/// A choice of microphone, camera or speaker, each with its kind's icon. With none attached it says so and stays shut.
|
||||
#[derive(IntoElement)]
|
||||
pub struct DeviceSelector {
|
||||
id: ElementId,
|
||||
kind: DeviceKind,
|
||||
devices: Vec<Device>,
|
||||
chosen: Option<SharedString>,
|
||||
on_change: Option<OnValue>,
|
||||
}
|
||||
|
||||
impl DeviceSelector {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
kind: DeviceKind,
|
||||
devices: impl IntoIterator<Item = Device>,
|
||||
) -> Self {
|
||||
let devices: Vec<Device> = devices.into_iter().collect();
|
||||
for (ix, device) in devices.iter().enumerate() {
|
||||
assert!(
|
||||
!devices[..ix].iter().any(|other| other.key == device.key),
|
||||
"device {} twice",
|
||||
device.key
|
||||
);
|
||||
}
|
||||
Self {
|
||||
id: id.into(),
|
||||
kind,
|
||||
devices,
|
||||
chosen: None,
|
||||
on_change: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The device in use, by key.
|
||||
pub fn chosen(mut self, key: impl Into<SharedString>) -> Self {
|
||||
let key = key.into();
|
||||
assert!(
|
||||
self.devices.iter().any(|device| device.key == key),
|
||||
"no device {key}"
|
||||
);
|
||||
self.chosen = Some(key);
|
||||
self
|
||||
}
|
||||
|
||||
/// Gets the key of the device to use.
|
||||
pub fn on_change(
|
||||
mut self,
|
||||
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_change = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for DeviceSelector {
|
||||
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
|
||||
let (kind, empty) = (self.kind, self.devices.is_empty());
|
||||
let choices = self
|
||||
.devices
|
||||
.into_iter()
|
||||
.map(|device| Choice::new(device.key, device.name).icon(kind.icon()));
|
||||
let placeholder = match empty {
|
||||
true => format!("No {} found", kind.noun()),
|
||||
false => format!("Choose a {}", kind.noun()),
|
||||
};
|
||||
let select = Select::new(self.id, choices)
|
||||
.placeholder(placeholder)
|
||||
.disabled(empty || self.on_change.is_none());
|
||||
let select = match self.chosen {
|
||||
Some(key) => select.selected(key),
|
||||
None => select,
|
||||
};
|
||||
match self.on_change {
|
||||
Some(change) => select.on_change(move |key, window, cx| {
|
||||
log::info!("device selector: {} {key}", kind.noun());
|
||||
change(key, window, cx)
|
||||
}),
|
||||
None => select,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user