Scrubber shows what has loaded, what has played and its chapters, names the time under the pointer, seeks by press and gpui's drag, and steps by the slider's keys; over_media lights it for a dark layer. VideoPlayer shows the host's frame in its shape with a caption and a control bar over it: the scrubber, play, the time, speed, captions, picture in picture and full screen, each only with its handler; the bar rests after a still pointer while it plays, on one wake from the executor's timer. Keys: Space and K, J and L, the arrows, C and F. VideoThumbnailStrip lays frames along the length under a playhead with a trim whose handles drag and whose ends I and O set. SubtitleEditor edits cues in place: times as mm:ss.mmm, kept in order, a time that does not read kept out with the reason shown. InlineEdit is a Tab stop with the ring; it hands focus back after Enter or Escape and keeps its text on Enter's release. overlays::media_button is shared by the lightbox and the player's bar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
399 lines
11 KiB
Rust
399 lines
11 KiB
Rust
use gpui::{
|
|
App, Hsla, IntoElement, Radians, RenderOnce, SharedString, Transformation, Window, prelude::*,
|
|
size, svg,
|
|
};
|
|
|
|
use crate::theme::{ActiveTheme, IconSize};
|
|
|
|
macro_rules! icons {
|
|
($($variant:ident => $file:literal,)*) => {
|
|
/// Lucide icons bundled with Ely.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
pub enum IconName {
|
|
$($variant,)*
|
|
}
|
|
|
|
impl IconName {
|
|
pub const ALL: &[IconName] = &[$(IconName::$variant,)*];
|
|
|
|
pub fn path(self) -> &'static str {
|
|
match self {
|
|
$(IconName::$variant => concat!("icons/", $file, ".svg"),)*
|
|
}
|
|
}
|
|
|
|
/// Lucide name, such as `chevron-down`.
|
|
pub fn name(self) -> &'static str {
|
|
match self {
|
|
$(IconName::$variant => $file,)*
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
icons! {
|
|
AArrowDown => "a-arrow-down",
|
|
AArrowUp => "a-arrow-up",
|
|
Accessibility => "accessibility",
|
|
Activity => "activity",
|
|
AlarmClock => "alarm-clock",
|
|
AlignCenter => "align-center",
|
|
AlignJustify => "align-justify",
|
|
AlignLeft => "align-left",
|
|
AlignRight => "align-right",
|
|
AppWindow => "app-window",
|
|
Archive => "archive",
|
|
ArrowDownLeft => "arrow-down-left",
|
|
ArrowDownToDot => "arrow-down-to-dot",
|
|
ArrowDownToLine => "arrow-down-to-line",
|
|
ArrowDownWideNarrow => "arrow-down-wide-narrow",
|
|
ArrowDown => "arrow-down",
|
|
ArrowLeft => "arrow-left",
|
|
ArrowRight => "arrow-right",
|
|
ArrowUpFromDot => "arrow-up-from-dot",
|
|
ArrowUpNarrowWide => "arrow-up-narrow-wide",
|
|
ArrowUpRight => "arrow-up-right",
|
|
ArrowUp => "arrow-up",
|
|
AtSign => "at-sign",
|
|
AudioLines => "audio-lines",
|
|
Ban => "ban",
|
|
Battery => "battery",
|
|
Bell => "bell",
|
|
Bold => "bold",
|
|
BookOpen => "book-open",
|
|
Bookmark => "bookmark",
|
|
Bot => "bot",
|
|
Box => "box",
|
|
Braces => "braces",
|
|
Brush => "brush",
|
|
Bug => "bug",
|
|
CalendarDays => "calendar-days",
|
|
CalendarRange => "calendar-range",
|
|
Calendar => "calendar",
|
|
Camera => "camera",
|
|
Captions => "captions",
|
|
CaptionsOff => "captions-off",
|
|
CaseSensitive => "case-sensitive",
|
|
ChartArea => "chart-area",
|
|
ChartBar => "chart-bar",
|
|
ChartCandlestick => "chart-candlestick",
|
|
ChartColumn => "chart-column",
|
|
ChartLine => "chart-line",
|
|
ChartPie => "chart-pie",
|
|
CheckCheck => "check-check",
|
|
Check => "check",
|
|
ChevronDown => "chevron-down",
|
|
ChevronLeft => "chevron-left",
|
|
ChevronRight => "chevron-right",
|
|
ChevronUp => "chevron-up",
|
|
ChevronsLeft => "chevrons-left",
|
|
ChevronsLeftRight => "chevrons-left-right",
|
|
ChevronsRight => "chevrons-right",
|
|
ChevronsUpDown => "chevrons-up-down",
|
|
CircleAlert => "circle-alert",
|
|
CircleCheck => "circle-check",
|
|
CircleDot => "circle-dot",
|
|
CircleHelp => "circle-help",
|
|
CirclePause => "circle-pause",
|
|
CircleStop => "circle-stop",
|
|
CircleX => "circle-x",
|
|
Circle => "circle",
|
|
ClipboardCheck => "clipboard-check",
|
|
Clipboard => "clipboard",
|
|
Clock => "clock",
|
|
CloudAlert => "cloud-alert",
|
|
CloudCheck => "cloud-check",
|
|
CloudOff => "cloud-off",
|
|
Cloud => "cloud",
|
|
CloudUpload => "cloud-upload",
|
|
Code => "code",
|
|
Columns2 => "columns-2",
|
|
Command => "command",
|
|
Compass => "compass",
|
|
Contrast => "contrast",
|
|
Copy => "copy",
|
|
CornerDownRight => "corner-down-right",
|
|
Cpu => "cpu",
|
|
CreditCard => "credit-card",
|
|
Crop => "crop",
|
|
Crosshair => "crosshair",
|
|
Database => "database",
|
|
Dices => "dices",
|
|
DollarSign => "dollar-sign",
|
|
Dot => "dot",
|
|
Download => "download",
|
|
EllipsisVertical => "ellipsis-vertical",
|
|
Ellipsis => "ellipsis",
|
|
Eraser => "eraser",
|
|
ExternalLink => "external-link",
|
|
EyeOff => "eye-off",
|
|
Eye => "eye",
|
|
FilePlus => "file-plus",
|
|
FileText => "file-text",
|
|
File => "file",
|
|
Filter => "filter",
|
|
Fingerprint => "fingerprint",
|
|
Flag => "flag",
|
|
FolderOpen => "folder-open",
|
|
FolderPlus => "folder-plus",
|
|
Folder => "folder",
|
|
Forward => "forward",
|
|
Frame => "frame",
|
|
Fullscreen => "fullscreen",
|
|
Gauge => "gauge",
|
|
GitBranch => "git-branch",
|
|
GitCommitHorizontal => "git-commit-horizontal",
|
|
GitMerge => "git-merge",
|
|
GitPullRequest => "git-pull-request",
|
|
GitPullRequestClosed => "git-pull-request-closed",
|
|
GitPullRequestDraft => "git-pull-request-draft",
|
|
Globe => "globe",
|
|
GripHorizontal => "grip-horizontal",
|
|
GripVertical => "grip-vertical",
|
|
Hand => "hand",
|
|
HardDrive => "hard-drive",
|
|
Hash => "hash",
|
|
Heading => "heading",
|
|
Heading1 => "heading-1",
|
|
Heading2 => "heading-2",
|
|
Heading3 => "heading-3",
|
|
Heart => "heart",
|
|
History => "history",
|
|
Hourglass => "hourglass",
|
|
House => "house",
|
|
Image => "image",
|
|
ImageOff => "image-off",
|
|
Inbox => "inbox",
|
|
Info => "info",
|
|
Italic => "italic",
|
|
Kanban => "kanban",
|
|
Key => "key",
|
|
Keyboard => "keyboard",
|
|
Languages => "languages",
|
|
Layers => "layers",
|
|
LayoutDashboard => "layout-dashboard",
|
|
LayoutGrid => "layout-grid",
|
|
Library => "library",
|
|
Lightbulb => "lightbulb",
|
|
Link => "link",
|
|
ListChecks => "list-checks",
|
|
ListOrdered => "list-ordered",
|
|
ListTodo => "list-todo",
|
|
List => "list",
|
|
LoaderCircle => "loader-circle",
|
|
LockOpen => "lock-open",
|
|
Lock => "lock",
|
|
LogIn => "log-in",
|
|
LogOut => "log-out",
|
|
Mail => "mail",
|
|
MapPin => "map-pin",
|
|
Map => "map",
|
|
Maximize2 => "maximize-2",
|
|
MemoryStick => "memory-stick",
|
|
Menu => "menu",
|
|
Merge => "merge",
|
|
MessageSquare => "message-square",
|
|
MessageSquareDiff => "message-square-diff",
|
|
MicOff => "mic-off",
|
|
Mic => "mic",
|
|
Minimize2 => "minimize-2",
|
|
Minus => "minus",
|
|
Monitor => "monitor",
|
|
Moon => "moon",
|
|
MousePointer2 => "mouse-pointer-2",
|
|
Move => "move",
|
|
Network => "network",
|
|
NotebookPen => "notebook-pen",
|
|
Package => "package",
|
|
Palette => "palette",
|
|
PanelBottom => "panel-bottom",
|
|
PanelLeft => "panel-left",
|
|
PanelRight => "panel-right",
|
|
Paperclip => "paperclip",
|
|
Pause => "pause",
|
|
PenLine => "pen-line",
|
|
PenTool => "pen-tool",
|
|
Pencil => "pencil",
|
|
PhoneOff => "phone-off",
|
|
Phone => "phone",
|
|
PictureInPicture2 => "picture-in-picture-2",
|
|
Pipette => "pipette",
|
|
PinOff => "pin-off",
|
|
Pin => "pin",
|
|
Play => "play",
|
|
Plus => "plus",
|
|
Power => "power",
|
|
Printer => "printer",
|
|
Puzzle => "puzzle",
|
|
Quote => "quote",
|
|
Redo2 => "redo-2",
|
|
RedoDot => "redo-dot",
|
|
RefreshCw => "refresh-cw",
|
|
Regex => "regex",
|
|
Reply => "reply",
|
|
Rocket => "rocket",
|
|
RotateCcw => "rotate-ccw",
|
|
RotateCw => "rotate-cw",
|
|
Rows2 => "rows-2",
|
|
Save => "save",
|
|
Scissors => "scissors",
|
|
Search => "search",
|
|
SearchX => "search-x",
|
|
Send => "send",
|
|
Server => "server",
|
|
Settings => "settings",
|
|
Shapes => "shapes",
|
|
Share2 => "share-2",
|
|
Shield => "shield",
|
|
Shuffle => "shuffle",
|
|
SkipBack => "skip-back",
|
|
SkipForward => "skip-forward",
|
|
SlidersHorizontal => "sliders-horizontal",
|
|
Smartphone => "smartphone",
|
|
Smile => "smile",
|
|
SmilePlus => "smile-plus",
|
|
Sparkles => "sparkles",
|
|
Spline => "spline",
|
|
SquareCheck => "square-check",
|
|
Square => "square",
|
|
SquareFunction => "square-function",
|
|
SquarePen => "square-pen",
|
|
Star => "star",
|
|
StepForward => "step-forward",
|
|
Strikethrough => "strikethrough",
|
|
Sun => "sun",
|
|
Table => "table",
|
|
Tag => "tag",
|
|
Target => "target",
|
|
Terminal => "terminal",
|
|
ThumbsDown => "thumbs-down",
|
|
ThumbsUp => "thumbs-up",
|
|
Timer => "timer",
|
|
Trash2 => "trash-2",
|
|
TrendingDown => "trending-down",
|
|
TrendingUp => "trending-up",
|
|
TriangleAlert => "triangle-alert",
|
|
Type => "type",
|
|
Underline => "underline",
|
|
Undo2 => "undo-2",
|
|
Upload => "upload",
|
|
User => "user",
|
|
UserPlus => "user-plus",
|
|
Users => "users",
|
|
Variable => "variable",
|
|
VideoOff => "video-off",
|
|
Video => "video",
|
|
Volume2 => "volume-2",
|
|
VolumeX => "volume-x",
|
|
Wallet => "wallet",
|
|
WandSparkles => "wand-sparkles",
|
|
WholeWord => "whole-word",
|
|
WifiOff => "wifi-off",
|
|
Wifi => "wifi",
|
|
Workflow => "workflow",
|
|
Wrench => "wrench",
|
|
X => "x",
|
|
Zap => "zap",
|
|
ZoomIn => "zoom-in",
|
|
ZoomOut => "zoom-out",
|
|
}
|
|
|
|
#[derive(IntoElement)]
|
|
pub struct Icon {
|
|
name: IconName,
|
|
size: IconSize,
|
|
color: Option<Hsla>,
|
|
hover: Option<(SharedString, Hsla)>,
|
|
rotation: Option<Radians>,
|
|
scale: f32,
|
|
}
|
|
|
|
impl Icon {
|
|
pub fn new(name: IconName) -> Self {
|
|
Self {
|
|
name,
|
|
size: IconSize::Md,
|
|
color: None,
|
|
hover: None,
|
|
rotation: None,
|
|
scale: 1.0,
|
|
}
|
|
}
|
|
|
|
pub fn size(mut self, size: IconSize) -> Self {
|
|
self.size = size;
|
|
self
|
|
}
|
|
|
|
/// Defaults to `colors.fg`.
|
|
pub fn color(mut self, color: impl Into<Hsla>) -> Self {
|
|
self.color = Some(color.into());
|
|
self
|
|
}
|
|
|
|
/// Color while the pointer is over `group`.
|
|
pub fn group_hover_color(
|
|
mut self,
|
|
group: impl Into<SharedString>,
|
|
color: impl Into<Hsla>,
|
|
) -> Self {
|
|
self.hover = Some((group.into(), color.into()));
|
|
self
|
|
}
|
|
|
|
pub fn rotate(mut self, angle: impl Into<Radians>) -> Self {
|
|
self.rotation = Some(angle.into());
|
|
self
|
|
}
|
|
|
|
/// Draws the glyph larger or smaller inside the same box.
|
|
pub fn scale(mut self, factor: f32) -> Self {
|
|
self.scale = factor;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl From<IconName> for Icon {
|
|
fn from(name: IconName) -> Self {
|
|
Icon::new(name)
|
|
}
|
|
}
|
|
|
|
impl RenderOnce for Icon {
|
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
let theme = cx.theme();
|
|
let color = self.color.unwrap_or(theme.colors.fg);
|
|
svg()
|
|
.path(self.name.path())
|
|
.size(theme.icon_size(self.size))
|
|
.flex_none()
|
|
.text_color(color)
|
|
.when_some(self.hover, |svg, (group, hover)| {
|
|
svg.group_hover(group, |style| style.text_color(hover))
|
|
})
|
|
.when(self.rotation.is_some() || self.scale != 1.0, |svg| {
|
|
svg.with_transformation(
|
|
Transformation::rotate(self.rotation.unwrap_or(Radians(0.0)))
|
|
.with_scaling(size(self.scale, self.scale)),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::IconName;
|
|
|
|
#[test]
|
|
fn every_icon_ships_in_the_bundle() {
|
|
for icon in IconName::ALL {
|
|
assert!(
|
|
crate::Assets::get(icon.path()).is_some(),
|
|
"{icon:?} missing at {}",
|
|
icon.path()
|
|
);
|
|
}
|
|
}
|
|
}
|