use std::sync::Arc;
use std::time::Duration;
use gpui::prelude::*;
use gpui::{
canvas, div, px, size, Bounds, ContentMask, Context, Corners, EventEmitter, Hsla, IntoElement,
RenderImage, SharedString, Window,
};
use super::{fit_rect, VideoFeed, VideoFit, VideoFrame};
use crate::devtools::Probed;
use crate::theme::{theme, Size};
const POLL: Duration = Duration::from_millis(8);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VideoEvent {
Resized { width: u32, height: u32 },
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct VideoStats {
pub shown: u64,
pub dropped: u64,
}
pub struct VideoView {
image: Option<Arc<RenderImage>>,
dims: Option<(u32, u32)>,
retired: Vec<Arc<RenderImage>>,
fit: VideoFit,
background: Option<Hsla>,
width: Option<f32>,
height: Option<f32>,
radius: Option<Size>,
placeholder: SharedString,
shown: u64,
feeds: Vec<VideoFeed>,
}
impl EventEmitter<VideoEvent> for VideoView {}
impl VideoView {
pub fn new(_cx: &mut Context<Self>) -> Self {
VideoView {
image: None,
dims: None,
retired: Vec::new(),
fit: VideoFit::default(),
background: None,
width: None,
height: None,
radius: None,
placeholder: "No signal".into(),
shown: 0,
feeds: Vec::new(),
}
}
pub fn fit(mut self, fit: VideoFit) -> Self {
self.fit = fit;
self
}
pub fn background(mut self, color: impl Into<Hsla>) -> Self {
self.background = Some(color.into());
self
}
pub fn width(mut self, width: f32) -> Self {
self.width = Some(width);
self
}
pub fn height(mut self, height: f32) -> Self {
self.height = Some(height);
self
}
pub fn radius(mut self, radius: Size) -> Self {
self.radius = Some(radius);
self
}
pub fn placeholder(mut self, text: impl Into<SharedString>) -> Self {
self.placeholder = text.into();
self
}
pub fn push_frame(&mut self, frame: VideoFrame, cx: &mut Context<Self>) {
let dims = (frame.width(), frame.height());
let Some(image) = frame.into_image() else {
return;
};
if let Some(old) = self.image.replace(image) {
self.retired.push(old);
}
self.shown += 1;
if self.dims.replace(dims) != Some(dims) {
cx.emit(VideoEvent::Resized {
width: dims.0,
height: dims.1,
});
}
cx.notify();
}
pub fn clear(&mut self, cx: &mut Context<Self>) {
if let Some(old) = self.image.take() {
self.retired.push(old);
}
self.dims = None;
cx.notify();
}
pub fn frame_size(&self) -> Option<(u32, u32)> {
self.dims
}
pub fn stats(&self) -> VideoStats {
let sent: u64 = self.feeds.iter().map(VideoFeed::sent).sum();
VideoStats {
shown: self.shown,
dropped: sent.saturating_sub(self.shown),
}
}
pub fn feed(&mut self, cx: &mut Context<Self>) -> VideoFeed {
let feed = VideoFeed::new();
let poller = feed.clone();
self.feeds.push(feed.clone());
cx.spawn(async move |this, cx| loop {
cx.background_executor().timer(POLL).await;
if poller.orphaned() {
break;
}
let Some(frame) = poller.take() else {
continue;
};
if this
.update(cx, |view, cx| view.push_frame(frame, cx))
.is_err()
{
break;
}
})
.detach();
feed
}
}
impl Render for VideoView {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
for old in self.retired.drain(..) {
let _ = window.drop_image(old);
}
let t = theme(cx);
let bg = self.background.unwrap_or_else(|| t.surface().hsla());
let dimmed = t.dimmed().hsla();
let radius = t.radius(self.radius.unwrap_or(t.default_radius));
let mut root = div()
.id("guise-videoview")
.flex()
.items_center()
.justify_center()
.overflow_hidden()
.bg(bg)
.rounded(px(radius));
root = match self.width {
Some(w) => root.w(px(w)),
None => root.w_full(),
};
root = match self.height {
Some(h) => root.h(px(h)),
None => root.h_full(),
};
let (image, dims) = (self.image.clone(), self.dims);
let fit = self.fit;
let root = match (image, dims) {
(Some(image), Some((w, h))) => root.child(
canvas(
|_, _, _| (),
move |bounds, _, window, _| {
let dst = (f32::from(bounds.size.width), f32::from(bounds.size.height));
let (x, y, fw, fh) = fit_rect((w as f32, h as f32), dst, fit);
let target = Bounds::new(
bounds.origin + gpui::point(px(x), px(y)),
size(px(fw), px(fh)),
);
let corners = if fw >= dst.0 - 0.5 && fh >= dst.1 - 0.5 {
Corners::all(px(radius))
} else {
Corners::default()
};
window.with_content_mask(Some(ContentMask { bounds }), |window| {
let _ = window.paint_image(target, corners, image.clone(), 0, false);
});
},
)
.size_full(),
),
_ => root.text_color(dimmed).child(self.placeholder.clone()),
};
root
.probe("VideoView")
.attr("fit", format!("{:?}", self.fit))
.attr("frames", format!("{}", self.shown))
}
}