-
Notifications
You must be signed in to change notification settings - Fork 8
render wallpaper on every connected display #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ mod screen_observer; | |
| use std::path::Path; | ||
|
|
||
| use anyhow::Context; | ||
| use objc2::rc::Retained; | ||
| use objc2::sel; | ||
| use objc2_app_kit::{ | ||
| NSApplication, NSApplicationActivationPolicy, | ||
|
|
@@ -49,39 +50,6 @@ impl Backend for MacosBackend { | |
| app.setActivationPolicy(NSApplicationActivationPolicy::Accessory); | ||
| app.finishLaunching(); | ||
|
|
||
| let screen = NSScreen::mainScreen(mtm).context("no main screen")?; | ||
| let frame = screen.frame(); | ||
| let backing_scale = screen.backingScaleFactor(); | ||
|
|
||
| let window = unsafe { | ||
| NSWindow::initWithContentRect_styleMask_backing_defer( | ||
| mtm.alloc::<NSWindow>(), | ||
| frame, | ||
| NSWindowStyleMask::Borderless, | ||
| NSBackingStoreType::Buffered, | ||
| false, | ||
| ) | ||
| }; | ||
| window.setLevel(WALLPAPER_LEVEL); | ||
| window.setCollectionBehavior( | ||
| NSWindowCollectionBehavior::CanJoinAllSpaces | ||
| | NSWindowCollectionBehavior::FullScreenAuxiliary | ||
| | NSWindowCollectionBehavior::Stationary | ||
| | NSWindowCollectionBehavior::IgnoresCycle, | ||
| ); | ||
| window.setOpaque(false); | ||
| window.setBackgroundColor(Some(&NSColor::clearColor())); | ||
| window.setHasShadow(false); | ||
| // `ReadOnly` so screen-capture / screen-sharing can read us. `None` blocks them. | ||
| window.setSharingType(NSWindowSharingType::ReadOnly); | ||
| window.setIgnoresMouseEvents(true); | ||
|
|
||
| let content_view = NSView::initWithFrame(mtm.alloc::<NSView>(), frame); | ||
| content_view.setWantsLayer(true); | ||
| window.setContentView(Some(&content_view)); | ||
|
|
||
| // AVAsset resolves relative paths against the process cwd, which isn't | ||
| // what users expect from `phonto ./video.mp4`. | ||
| let abs = Path::new(&video_path) | ||
| .canonicalize() | ||
| .unwrap_or_else(|_| Path::new(&video_path).to_path_buf()); | ||
|
|
@@ -94,23 +62,22 @@ impl Backend for MacosBackend { | |
| player.setMuted(true); | ||
| } | ||
|
|
||
| let player_layer = unsafe { AVPlayerLayer::playerLayerWithPlayer(Some(&player)) }; | ||
| if let Some(gravity) = video_gravity_for(scale) { | ||
| unsafe { player_layer.setVideoGravity(gravity) }; | ||
| let screens = NSScreen::screens(mtm); | ||
| let screen_count = screens.count(); | ||
| if screen_count == 0 { | ||
| return Err(anyhow::anyhow!("no screens found")); | ||
| } | ||
| player_layer.setFrame(content_view.bounds()); | ||
| player_layer.setAutoresizingMask( | ||
| CAAutoresizingMask::LayerWidthSizable | CAAutoresizingMask::LayerHeightSizable, | ||
| ); | ||
| player_layer.setContentsScale(backing_scale); | ||
|
|
||
| let root_layer = content_view | ||
| .layer() | ||
| .context("content view has no root layer")?; | ||
| root_layer.addSublayer(&player_layer); | ||
| let gravity = video_gravity_for(scale); | ||
| let mut windows = Vec::with_capacity(screen_count); | ||
| let mut player_layers = Vec::with_capacity(screen_count); | ||
| for i in 0..screen_count { | ||
| let screen = screens.objectAtIndex(i); | ||
| let (window, layer) = build_wallpaper_window(mtm, &screen, &player, gravity)?; | ||
| windows.push(window); | ||
| player_layers.push(layer); | ||
| } | ||
|
|
||
| // Loop: AVPlayer posts AVPlayerItemDidPlayToEndTimeNotification when the | ||
| // item reaches its end. The observer seeks back to zero and resumes. | ||
| let loop_observer = LoopObserver::new(player.clone()); | ||
| unsafe { | ||
| NSNotificationCenter::defaultCenter().addObserver_selector_name_object( | ||
|
|
@@ -121,8 +88,7 @@ impl Backend for MacosBackend { | |
| ); | ||
| } | ||
|
|
||
| // Re-apply geometry on display reconfiguration. | ||
| let screen_observer = ScreenObserver::new(window.clone(), player_layer.clone()); | ||
| let screen_observer = ScreenObserver::new(windows, player_layers); | ||
| unsafe { | ||
| NSNotificationCenter::defaultCenter().addObserver_selector_name_object( | ||
| &screen_observer, | ||
|
|
@@ -132,8 +98,6 @@ impl Backend for MacosBackend { | |
| ); | ||
| } | ||
|
|
||
| window.makeKeyAndOrderFront(None); | ||
|
|
||
| let battery_observer = if matches!(options.pause, PauseMode::Never) { | ||
| None | ||
| } else { | ||
|
|
@@ -144,9 +108,8 @@ impl Backend for MacosBackend { | |
| } | ||
|
|
||
| log::info!( | ||
| "macOS backend ready: {}x{} window at level {}", | ||
| frame.size.width as u32, | ||
| frame.size.height as u32, | ||
| "macOS backend ready: {} screen(s) at level {}", | ||
| screen_count, | ||
| WALLPAPER_LEVEL, | ||
| ); | ||
|
|
||
|
|
@@ -170,3 +133,57 @@ fn video_gravity_for(scale: ScaleMode) -> Option<&'static AVLayerVideoGravity> { | |
| } | ||
| } | ||
| } | ||
|
|
||
| fn build_wallpaper_window( | ||
| mtm: MainThreadMarker, | ||
| screen: &NSScreen, | ||
| player: &AVPlayer, | ||
| gravity: Option<&'static AVLayerVideoGravity>, | ||
| ) -> anyhow::Result<(Retained<NSWindow>, Retained<AVPlayerLayer>)> { | ||
| let frame = screen.frame(); | ||
| let backing_scale = screen.backingScaleFactor(); | ||
|
|
||
| let window = unsafe { | ||
| NSWindow::initWithContentRect_styleMask_backing_defer( | ||
| mtm.alloc::<NSWindow>(), | ||
| frame, | ||
| NSWindowStyleMask::Borderless, | ||
| NSBackingStoreType::Buffered, | ||
| false, | ||
| ) | ||
| }; | ||
| window.setLevel(WALLPAPER_LEVEL); | ||
| window.setCollectionBehavior( | ||
| NSWindowCollectionBehavior::CanJoinAllSpaces | ||
| | NSWindowCollectionBehavior::FullScreenAuxiliary | ||
| | NSWindowCollectionBehavior::Stationary | ||
| | NSWindowCollectionBehavior::IgnoresCycle, | ||
| ); | ||
| window.setOpaque(false); | ||
| window.setBackgroundColor(Some(&NSColor::clearColor())); | ||
| window.setHasShadow(false); | ||
| window.setSharingType(NSWindowSharingType::ReadOnly); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The original comment about why we use |
||
| window.setIgnoresMouseEvents(true); | ||
|
|
||
| let content_view = NSView::initWithFrame(mtm.alloc::<NSView>(), frame); | ||
| content_view.setWantsLayer(true); | ||
| window.setContentView(Some(&content_view)); | ||
|
|
||
| let layer = unsafe { AVPlayerLayer::playerLayerWithPlayer(Some(player)) }; | ||
| if let Some(gravity) = gravity { | ||
| unsafe { layer.setVideoGravity(gravity) }; | ||
| } | ||
| layer.setFrame(content_view.bounds()); | ||
| layer.setAutoresizingMask( | ||
| CAAutoresizingMask::LayerWidthSizable | CAAutoresizingMask::LayerHeightSizable, | ||
| ); | ||
| layer.setContentsScale(backing_scale); | ||
|
|
||
| let root_layer = content_view | ||
| .layer() | ||
| .context("content view has no root layer")?; | ||
| root_layer.addSublayer(&layer); | ||
|
|
||
| window.makeKeyAndOrderFront(None); | ||
| Ok((window, layer)) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,11 @@ | ||
| use objc2::{AnyThread, DefinedClass, define_class, msg_send, rc::Retained, runtime::NSObject}; | ||
| use objc2_app_kit::{NSScreen, NSWindow}; | ||
| use objc2_app_kit::NSWindow; | ||
| use objc2_av_foundation::AVPlayerLayer; | ||
| use objc2_foundation::{MainThreadMarker, NSNotification}; | ||
| use objc2_foundation::NSNotification; | ||
|
|
||
| pub struct ScreenObserverIvars { | ||
| window: Retained<NSWindow>, | ||
| layer: Retained<AVPlayerLayer>, | ||
| windows: Vec<Retained<NSWindow>>, | ||
| layers: Vec<Retained<AVPlayerLayer>>, | ||
| } | ||
|
|
||
| define_class!( | ||
|
|
@@ -22,32 +22,31 @@ define_class!( | |
| ); | ||
|
|
||
| impl ScreenObserver { | ||
| pub fn new(window: Retained<NSWindow>, layer: Retained<AVPlayerLayer>) -> Retained<Self> { | ||
| let ivars = ScreenObserverIvars { window, layer }; | ||
| pub fn new( | ||
| windows: Vec<Retained<NSWindow>>, | ||
| layers: Vec<Retained<AVPlayerLayer>>, | ||
| ) -> Retained<Self> { | ||
| let ivars = ScreenObserverIvars { windows, layers }; | ||
| let this = Self::alloc().set_ivars(ivars); | ||
| unsafe { msg_send![super(this), init] } | ||
| } | ||
|
|
||
| fn apply_current_screen(&self) { | ||
| let Some(mtm) = MainThreadMarker::new() else { | ||
| return; | ||
| }; | ||
| let Some(screen) = NSScreen::mainScreen(mtm) else { | ||
| return; | ||
| }; | ||
|
|
||
| let frame = screen.frame(); | ||
| let backing_scale = screen.backingScaleFactor(); | ||
|
|
||
| let ivars = self.ivars(); | ||
| ivars.window.setFrame_display(frame, false); | ||
| ivars.layer.setContentsScale(backing_scale); | ||
|
|
||
| log::info!( | ||
| "display reconfigured: {}x{} @ {}x backing", | ||
| frame.size.width as u32, | ||
| frame.size.height as u32, | ||
| backing_scale, | ||
| ); | ||
| for (window, layer) in ivars.windows.iter().zip(ivars.layers.iter()) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This only repositions existing windows. If a new display is plugged in after launch, no window or layer gets created for it and that screen stays bare. Worth a follow up to rebuild the list when screen parameters change. |
||
| let Some(screen) = window.screen() else { | ||
| continue; | ||
| }; | ||
| let frame = screen.frame(); | ||
| let backing_scale = screen.backingScaleFactor(); | ||
| window.setFrame_display(frame, false); | ||
| layer.setContentsScale(backing_scale); | ||
| log::info!( | ||
| "display reconfigured: {}x{} @ {}x backing", | ||
| frame.size.width as u32, | ||
| frame.size.height as u32, | ||
| backing_scale, | ||
| ); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The original comment about why we canonicalize got dropped in this refactor. It explained that
AVAssetresolves relative paths against the process cwd, which isn't what users expect fromphonto ./video.mp4. Worth restoring.