diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index dc8d0091343403363ec50f9b7a2bdf48770545d3..4c7c68942ed9d4d2e2df0971b0f541269afae7e8 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -43,8 +43,8 @@ use crate::{ Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, Priority, PromptBuilder, PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource, SharedString, SubscriberSet, - Subscription, SvgRenderer, Task, TextRenderingMode, TextSystem, Window, WindowAppearance, - WindowHandle, WindowId, WindowInvalidator, + Subscription, SvgRenderer, Task, TextRenderingMode, TextSystem, ThermalState, Window, + WindowAppearance, WindowHandle, WindowId, WindowInvalidator, colors::{Colors, GlobalColors}, current_platform, hash, init_app_menus, }; @@ -618,6 +618,7 @@ pub struct App { pub(crate) keystroke_observers: SubscriberSet<(), KeystrokeObserver>, pub(crate) keystroke_interceptors: SubscriberSet<(), KeystrokeObserver>, pub(crate) keyboard_layout_observers: SubscriberSet<(), Handler>, + pub(crate) thermal_state_observers: SubscriberSet<(), Handler>, pub(crate) release_listeners: SubscriberSet, pub(crate) global_observers: SubscriberSet, pub(crate) quit_observers: SubscriberSet<(), QuitHandler>, @@ -702,6 +703,7 @@ impl App { keystroke_observers: SubscriberSet::new(), keystroke_interceptors: SubscriberSet::new(), keyboard_layout_observers: SubscriberSet::new(), + thermal_state_observers: SubscriberSet::new(), global_observers: SubscriberSet::new(), quit_observers: SubscriberSet::new(), restart_observers: SubscriberSet::new(), @@ -740,6 +742,18 @@ impl App { } })); + platform.on_thermal_state_change(Box::new({ + let app = Rc::downgrade(&app); + move || { + if let Some(app) = app.upgrade() { + let cx = &mut app.borrow_mut(); + cx.thermal_state_observers + .clone() + .retain(&(), move |callback| (callback)(cx)); + } + } + })); + platform.on_quit(Box::new({ let cx = app.clone(); move || { @@ -1082,6 +1096,27 @@ impl App { .cloned() } + /// Returns the current thermal state of the system. + pub fn thermal_state(&self) -> ThermalState { + self.platform.thermal_state() + } + + /// Invokes a handler when the thermal state changes + pub fn on_thermal_state_change(&self, mut callback: F) -> Subscription + where + F: 'static + FnMut(&mut App), + { + let (subscription, activate) = self.thermal_state_observers.insert( + (), + Box::new(move |cx| { + callback(cx); + true + }), + ); + activate(); + subscription + } + /// Returns the appearance of the application's windows. pub fn window_appearance(&self) -> WindowAppearance { self.platform.window_appearance() diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index df72d4dc51286187909fe6081cc7dd3c27bb81b3..dbd630896f111d12ed4cdc44a5400021f26cf67f 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -261,6 +261,9 @@ pub(crate) trait Platform: 'static { fn on_will_open_app_menu(&self, callback: Box); fn on_validate_app_menu_command(&self, callback: Box bool>); + fn thermal_state(&self) -> ThermalState; + fn on_thermal_state_change(&self, callback: Box); + fn compositor_name(&self) -> &'static str { "" } @@ -323,6 +326,19 @@ pub trait PlatformDisplay: Send + Sync + Debug { } } +/// Thermal state of the system +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ThermalState { + /// System has no thermal constraints + Nominal, + /// System is slightly constrained, reduce discretionary work + Fair, + /// System is moderately constrained, reduce CPU/GPU intensive work + Serious, + /// System is critically constrained, minimize all resource usage + Critical, +} + /// Metadata for a given [ScreenCaptureSource] #[derive(Clone)] pub struct SourceMetadata { diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 06a81ec342e9d528a081456583f3ba0f3fb77b6f..5f328cd928073764a39db34400279949c608a368 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -26,7 +26,7 @@ use crate::{ ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PlatformWindow, Point, PriorityQueueCalloopReceiver, Result, - RunnableVariant, Task, WindowAppearance, WindowParams, px, + RunnableVariant, Task, ThermalState, WindowAppearance, WindowParams, px, }; #[cfg(any(feature = "wayland", feature = "x11"))] @@ -203,6 +203,12 @@ impl Platform for P { self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback)); } + fn on_thermal_state_change(&self, _callback: Box) {} + + fn thermal_state(&self) -> ThermalState { + ThermalState::Nominal + } + fn run(&self, on_finish_launching: Box) { on_finish_launching(); diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index f66f9ecd1be18d3dc78d8436a92997414eca9f00..041a914251127b202cd29d4ae773c4eaef12964a 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -5,8 +5,8 @@ use crate::{ Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor, KeyContext, Keymap, MacDispatcher, MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, - PlatformTextSystem, PlatformWindow, Result, SystemMenuType, Task, WindowAppearance, - WindowParams, platform::mac::pasteboard::Pasteboard, + PlatformTextSystem, PlatformWindow, Result, SystemMenuType, Task, ThermalState, + WindowAppearance, WindowParams, platform::mac::pasteboard::Pasteboard, }; use anyhow::{Context as _, anyhow}; use block::ConcreteBlock; @@ -144,6 +144,11 @@ unsafe fn build_classes() { on_keyboard_layout_change as extern "C" fn(&mut Object, Sel, id), ); + decl.add_method( + sel!(onThermalStateChange:), + on_thermal_state_change as extern "C" fn(&mut Object, Sel, id), + ); + decl.register() } } @@ -161,6 +166,7 @@ pub(crate) struct MacPlatformState { find_pasteboard: Pasteboard, reopen: Option>, on_keyboard_layout_change: Option>, + on_thermal_state_change: Option>, quit: Option>, menu_command: Option>, validate_menu_command: Option bool>>, @@ -204,6 +210,7 @@ impl MacPlatform { finish_launching: None, dock_menu: None, on_keyboard_layout_change: None, + on_thermal_state_change: None, menus: None, keyboard_mapper, })) @@ -877,6 +884,24 @@ impl Platform for MacPlatform { self.0.lock().validate_menu_command = Some(callback); } + fn on_thermal_state_change(&self, callback: Box) { + self.0.lock().on_thermal_state_change = Some(callback); + } + + fn thermal_state(&self) -> ThermalState { + unsafe { + let process_info: id = msg_send![class!(NSProcessInfo), processInfo]; + let state: NSInteger = msg_send![process_info, thermalState]; + match state { + 0 => ThermalState::Nominal, + 1 => ThermalState::Fair, + 2 => ThermalState::Serious, + 3 => ThermalState::Critical, + _ => ThermalState::Nominal, + } + } + } + fn keyboard_layout(&self) -> Box { Box::new(MacKeyboardLayout::new()) } @@ -1178,6 +1203,14 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) { object: nil ]; + let thermal_name = ns_string("NSProcessInfoThermalStateDidChangeNotification"); + let process_info: id = msg_send![class!(NSProcessInfo), processInfo]; + let _: () = msg_send![notification_center, addObserver: this as id + selector: sel!(onThermalStateChange:) + name: thermal_name + object: process_info + ]; + let platform = get_mac_platform(this); let callback = platform.0.lock().finish_launching.take(); if let Some(callback) = callback { @@ -1224,6 +1257,20 @@ extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) { } } +extern "C" fn on_thermal_state_change(this: &mut Object, _: Sel, _: id) { + let platform = unsafe { get_mac_platform(this) }; + let mut lock = platform.0.lock(); + if let Some(mut callback) = lock.on_thermal_state_change.take() { + drop(lock); + callback(); + platform + .0 + .lock() + .on_thermal_state_change + .get_or_insert(callback); + } +} + extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) { let urls = unsafe { (0..urls.count()) diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index b014a7dbfb9b86307b68841e086171aa2d40d332..06963ba23cb291f601297aa5b682e7ed1c85fee8 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -3,7 +3,7 @@ use crate::{ DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task, - TestDisplay, TestWindow, WindowAppearance, WindowParams, size, + TestDisplay, TestWindow, ThermalState, WindowAppearance, WindowParams, size, }; use anyhow::Result; use collections::VecDeque; @@ -246,6 +246,12 @@ impl Platform for TestPlatform { fn on_keyboard_layout_change(&self, _: Box) {} + fn on_thermal_state_change(&self, _: Box) {} + + fn thermal_state(&self) -> ThermalState { + ThermalState::Nominal + } + fn run(&self, _on_finish_launching: Box) { unimplemented!() } diff --git a/crates/gpui/src/platform/visual_test.rs b/crates/gpui/src/platform/visual_test.rs index df7989dcc24881ec8db1c57717a4b2e442c0e2c2..0329e8b45bf3945be903381bf5ba89f4f350d8b8 100644 --- a/crates/gpui/src/platform/visual_test.rs +++ b/crates/gpui/src/platform/visual_test.rs @@ -250,4 +250,10 @@ impl Platform for VisualTestPlatform { } fn on_keyboard_layout_change(&self, _callback: Box) {} + + fn thermal_state(&self) -> super::ThermalState { + super::ThermalState::Nominal + } + + fn on_thermal_state_change(&self, _callback: Box) {} } diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 262e1d6bb0dfe07f724baed115d0184857388b74..1e69c7d8f9d0a9ed305fc8e5e7d388b51246cfc4 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -394,6 +394,12 @@ impl Platform for WindowsPlatform { .set(Some(callback)); } + fn on_thermal_state_change(&self, _callback: Box) {} + + fn thermal_state(&self) -> ThermalState { + ThermalState::Nominal + } + fn run(&self, on_finish_launching: Box) { on_finish_launching(); if !self.headless { diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 6d3818abdaab9a3b9772ffd1186009f0be21761b..8be96c418f49262db6a57b6ca5ad0f30424d36c9 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -14,7 +14,7 @@ use crate::{ Replay, ResizeEdge, SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS_X, SUBPIXEL_VARIANTS_Y, ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style, SubpixelSprite, SubscriberSet, Subscription, SystemWindowTab, SystemWindowTabController, TabStopMap, - TaffyLayoutEngine, Task, TextRenderingMode, TextStyle, TextStyleRefinement, + TaffyLayoutEngine, Task, TextRenderingMode, TextStyle, TextStyleRefinement, ThermalState, TransformationMatrix, Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, point, prelude::*, px, rems, size, transparent_black, @@ -1155,6 +1155,7 @@ impl Window { let needs_present = Rc::new(Cell::new(false)); let next_frame_callbacks: Rc>> = Default::default(); let input_rate_tracker = Rc::new(RefCell::new(InputRateTracker::default())); + let last_frame_time = Rc::new(Cell::new(None)); platform_window .request_decorations(window_decorations.unwrap_or(WindowDecorations::Server)); @@ -1184,6 +1185,20 @@ impl Window { let next_frame_callbacks = next_frame_callbacks.clone(); let input_rate_tracker = input_rate_tracker.clone(); move |request_frame_options| { + let thermal_state = cx.update(|cx| cx.thermal_state()); + + if thermal_state == ThermalState::Serious || thermal_state == ThermalState::Critical + { + let now = Instant::now(); + let last_frame_time = last_frame_time.replace(Some(now)); + + if let Some(last_frame) = last_frame_time + && now.duration_since(last_frame) < Duration::from_micros(16667) + { + return; + } + } + let next_frame_callbacks = next_frame_callbacks.take(); if !next_frame_callbacks.is_empty() { handle