gpui_kit/media/mod.rs
1//! Sound, moving pictures, and geometry — none of it decoded here.
2//!
3//! The three surfaces in this module are the ones a general-purpose desktop
4//! application needs and cannot assemble out of a button and a slider: a
5//! player for audio, a player for video, and a viewer for a 3D model. They
6//! keep the posture the rest of the library keeps, which for media means
7//! something specific.
8//!
9//! **There is no player in this crate, and there is none in GPUI at the
10//! pinned revision.** GPUI draws images and, on macOS, composites a
11//! `CVPixelBuffer` through its surface element; it has no decoder, no audio
12//! device, and no frame pump. So [`AudioPlayer`] and [`VideoPlayer`] are
13//! written against [`MediaTransport`], the seam an operating-system backend
14//! lands behind, and a surface with no transport says exactly that instead of
15//! drawing a transport bar that would move if anything were playing.
16//!
17//! **A progress bar is never drawn from a guess.** Position, duration, and
18//! buffered spans are the transport's facts. A transport that reports no
19//! duration gets a position and no fraction, and a surface with no transport
20//! gets no track at all.
21//!
22//! **A fixture says so.** [`FixtureTransport`] decodes nothing and advances no
23//! clock. Every surface publishes and draws [`MediaOrigin`], so a scene, a
24//! test, and a screenshot all distinguish a fixture from a player.
25//!
26//! **A model is read inside a fence.** [`ModelViewer`] takes a glTF 2.0
27//! document through the bounded reader in [`gltf`], which accepts a stated
28//! subset, refuses anything outside it, and refuses anything past
29//! [`ModelBounds`] before allocating for it. The refusal is the contract; the
30//! shading is deliberately minimal.
31
32pub mod audio_player;
33pub mod gltf;
34pub mod model_viewer;
35pub mod transport;
36pub mod video_player;
37
38pub use audio_player::AudioPlayer;
39pub use gltf::{ModelBounds, ModelDefect, ModelError, ModelLimit, ModelMesh, ModelScene};
40pub use model_viewer::{ModelShading, ModelState, ModelViewer, ModelViewerEvent};
41pub use transport::{
42 FixtureTransport, MediaAvailability, MediaCommand, MediaEvent, MediaOrigin, MediaOutcome,
43 MediaSnapshot, MediaTransport,
44};
45pub use video_player::VideoPlayer;
46
47use gpui::{AnyElement, Hsla, IntoElement, ParentElement, SharedString, Styled, div, px};
48use gpui_kit_theme::{Space, Theme, TypeScale};
49
50use crate::foundation::{StyledExt, text};
51
52/// Where a notice sits on the surface it covers.
53#[derive(Clone, Copy, PartialEq, Eq)]
54pub(crate) enum NoticePlace {
55 /// The middle of an otherwise empty surface.
56 Middle,
57 /// A band along the foot, for a surface with a still behind it that the
58 /// reader is meant to keep seeing.
59 Foot,
60}
61
62/// What stands where the media would be when there is none.
63///
64/// A title and the backend's own sentence, never an empty rectangle: a reader
65/// shown a blank frame cannot tell a refusal from silence.
66fn notice(theme: &Theme, tint: Hsla, title: SharedString, detail: SharedString) -> AnyElement {
67 notice_at(theme, tint, title, detail, NoticePlace::Middle)
68}
69
70/// The same sentence, placed against whatever is already on the surface.
71fn notice_at(
72 theme: &Theme,
73 tint: Hsla,
74 title: SharedString,
75 detail: SharedString,
76 place: NoticePlace,
77) -> AnyElement {
78 let base = match place {
79 NoticePlace::Middle => div().absolute().inset_0().justify_center(),
80 // A still is worth keeping visible, so the sentence takes a band at
81 // the foot on a scrim of its own rather than covering the picture.
82 NoticePlace::Foot => div()
83 .absolute()
84 .bottom_0()
85 .left_0()
86 .right_0()
87 .bg(theme.colors.canvas.opacity(0.88)),
88 };
89 base.column()
90 .items_center()
91 .gap_token(theme, Space::Xs)
92 .p_token(theme, Space::Lg)
93 .text_align(gpui::TextAlign::Center)
94 .child(text(theme, TypeScale::Subtitle, title))
95 .child(
96 text(theme, TypeScale::Body, detail)
97 .max_w(px(360.0))
98 .text_color(tint),
99 )
100 .into_any_element()
101}