Ely Office: six offline-first desktop apps in Rust and GPUI

ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git),
ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-08 21:20:08 -04:00
commit b7fca0146d
817 files changed
+143101

No files matched your search

+126
View File
@@ -0,0 +1,126 @@
use std::path::Path;
use objc2::{AllocAnyThread, MainThreadMarker, rc::Retained, runtime::AnyObject};
use objc2_av_foundation::{
AVPlayer, AVPlayerItem, AVPlayerItemOutput, AVPlayerItemStatus, AVPlayerItemVideoOutput,
};
use objc2_core_media::{CMTime, CMTimeFlags};
use objc2_core_video::kCVPixelBufferPixelFormatTypeKey;
use objc2_foundation::{NSDictionary, NSNumber, NSString, NSURL};
use super::State;
/// Biplanar full-range YCbCr, as GPUI's surfaces draw.
const PIXELS: u32 = u32::from_be_bytes(*b"420f");
pub struct Player {
player: Retained<AVPlayer>,
item: Retained<AVPlayerItem>,
output: Retained<AVPlayerItemVideoOutput>,
}
fn seconds(time: CMTime) -> Option<f64> {
(time.flags.contains(CMTimeFlags::Valid) && !time.flags.contains(CMTimeFlags::Indefinite))
.then(|| time.value as f64 / time.timescale as f64)
}
impl Player {
pub fn open(path: &Path) -> anyhow::Result<Self> {
let mtm = MainThreadMarker::new()
.ok_or_else(|| anyhow::anyhow!("media plays on the main thread"))?;
let url = NSURL::fileURLWithPath(&NSString::from_str(&path.to_string_lossy()));
let player = unsafe { AVPlayer::playerWithURL(&url, mtm) };
let item = unsafe { player.currentItem() }
.ok_or_else(|| anyhow::anyhow!("no player item for {}", path.display()))?;
// The key is a CFString; NSString is its toll-free twin.
let key: &NSString =
unsafe { &*(kCVPixelBufferPixelFormatTypeKey as *const _ as *const NSString) };
let format = NSNumber::new_u32(PIXELS);
let value: &AnyObject = &format;
let attributes = NSDictionary::<NSString, AnyObject>::from_slices(&[key], &[value]);
let output = unsafe {
AVPlayerItemVideoOutput::initWithPixelBufferAttributes(
AVPlayerItemVideoOutput::alloc(),
Some(&attributes),
)
};
unsafe { item.addOutput(&output as &AVPlayerItemOutput) };
Ok(Player {
player,
item,
output,
})
}
pub fn state(&self) -> State {
match unsafe { self.item.status() } {
AVPlayerItemStatus::ReadyToPlay => {
let size = unsafe { self.item.presentationSize() };
let video = (size.width > 0.0 && size.height > 0.0)
.then(|| (size.width.round() as u32, size.height.round() as u32));
State::Ready {
seconds: seconds(unsafe { self.item.duration() }).unwrap_or(0.0),
video,
}
}
AVPlayerItemStatus::Failed => {
let reason = unsafe { self.item.error() }
.map(|error| error.localizedDescription().to_string())
.unwrap_or_else(|| "the system player gave no reason".into());
State::Failed(reason)
}
_ => State::Loading,
}
}
pub fn play(&self) {
unsafe { self.player.play() }
}
pub fn pause(&self) {
unsafe { self.player.pause() }
}
pub fn playing(&self) -> bool {
(unsafe { self.player.rate() }) > 0.0
}
pub fn position(&self) -> f64 {
seconds(unsafe { self.player.currentTime() }).unwrap_or(0.0)
}
pub fn seek(&self, to: f64) {
let time = CMTime {
value: (to * 600.0).round() as i64,
timescale: 600,
flags: CMTimeFlags::Valid,
epoch: 0,
};
let exact = CMTime {
value: 0,
timescale: 1,
flags: CMTimeFlags::Valid,
epoch: 0,
};
unsafe {
self.player
.seekToTime_toleranceBefore_toleranceAfter(time, exact, exact)
}
}
pub fn frame(&self) -> Option<super::Frame> {
let now = unsafe { self.item.currentTime() };
if !unsafe { self.output.hasNewPixelBufferForItemTime(now) } {
return None;
}
let buffer = unsafe {
self.output
.copyPixelBufferForItemTime_itemTimeForDisplay(now, std::ptr::null_mut())
}?;
let raw = Retained::into_raw(buffer);
// The copy hands its one reference over.
Some(unsafe {
<core_video::pixel_buffer::CVPixelBuffer as core_foundation::base::TCFType>::wrap_under_create_rule(raw.cast())
})
}
}