diff --git a/src/app.rs b/src/app.rs index ba74701..43de72e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use crate::app::apps::{App, AppCommand, ICNS_ICON}; use crate::commands::Function; -use crate::config::{Config, MainPage, Shelly, ThemeMode}; +use crate::config::{Config, MainPage, Position, Shelly, ThemeMode}; use crate::debounce::DebouncePolicy; use crate::platform::macos::launching::Shortcut; use crate::utils::icns_data_to_handle; @@ -177,6 +177,7 @@ pub enum SetConfigFields { ToDefault, ToggleHotkey(String), ClipboardHotkey(String), + SetPosition(Position), PlaceHolder(String), SearchUrl(String), ClipboardHistory(bool), diff --git a/src/app/pages/settings.rs b/src/app/pages/settings.rs index 203d21f..2fb8036 100644 --- a/src/app/pages/settings.rs +++ b/src/app/pages/settings.rs @@ -10,12 +10,16 @@ use iced::widget::Slider; use iced::widget::Space; use iced::widget::TextInput; use iced::widget::button; +use iced::widget::pick_list; use iced::widget::radio; use iced::widget::scrollable::Direction; use iced::widget::scrollable::Scrollbar; use iced::widget::text_input; use iced::widget::toggler; +use crate::config::Position; +use crate::styles::picklist_menu_style; +use crate::styles::picklist_style; use crate::styles::settings_contents_container_style; use crate::styles::settings_tabs_container_style; use crate::styles::settings_toggle_style; @@ -388,6 +392,23 @@ fn general_tab(config: Box, theme: crate::config::Theme) -> Column<'stat .into(), ])); + let theme_clone = theme.clone(); + let theme_clone_2 = theme.clone(); + let position_dropdown = settings_row_without_reset(settings_item_row([ + settings_hint_text( + theme.clone(), + "Window Position", + Some("The position of the window"), + ), + Space::new().width(Length::Fill).into(), + pick_list(Position::variants(), Some(config.window_location), |pos| { + Message::SetConfig(SetConfigFields::SetPosition(pos)) + }) + .style(move |_, status| picklist_style(&theme_clone, status)) + .menu_style(move |_| picklist_menu_style(&theme_clone_2)) + .into(), + ])); + Column::from_iter([ hotkey, cb_hotkey, @@ -395,6 +416,7 @@ fn general_tab(config: Box, theme: crate::config::Theme) -> Column<'stat search, debounce, start_at_login, + position_dropdown, auto_update, haptic, tray_icon, diff --git a/src/app/tile.rs b/src/app/tile.rs index fee0de4..d70928f 100644 --- a/src/app/tile.rs +++ b/src/app/tile.rs @@ -296,7 +296,7 @@ impl Tile { } else if modifiers.command() { s.parse::() .ok() - .filter(|&n| n >= 1 && n <= 9) + .filter(|&n| (1..=9).contains(&n)) .map(|n| Message::OpenResult((n - 1) as u32)) } else if s == "p" && modifiers.control() { Some(Message::ChangeFocus(ArrowKey::Up, 1)) diff --git a/src/app/tile/elm.rs b/src/app/tile/elm.rs index 07f9356..b20e415 100644 --- a/src/app/tile/elm.rs +++ b/src/app/tile/elm.rs @@ -41,8 +41,13 @@ pub fn new(hotkeys: Hotkeys, config: &Config) -> (Tile, Task) { let events = Event::get_events(config.event_duration); - let open = open.discard().chain(window::run(id, |handle| { - platform::window_config(&handle.window_handle().expect("Unable to get window handle")); + let pos = config.window_location; + + let open = open.discard().chain(window::run(id, move |handle| { + platform::window_config( + &handle.window_handle().expect("Unable to get window handle"), + pos, + ); transform_process_to_ui_element(); })); info!("MacOS platform config applied"); diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs index 6c29cec..b2b5c03 100644 --- a/src/app/tile/update.rs +++ b/src/app/tile/update.rs @@ -12,6 +12,8 @@ use iced::widget::operation::AbsoluteOffset; use iced::window; use iced::window::Id; use log::info; +use objc2::MainThreadMarker; +use objc2_app_kit::NSApplication; use rayon::iter::IntoParallelRefIterator; use rayon::iter::ParallelIterator; use rayon::slice::ParallelSliceMut; @@ -39,11 +41,13 @@ use crate::calculator::Expr; use crate::commands::Function; use crate::config::Config; use crate::config::MainPage; +use crate::config::Position; use crate::config::ThemeMode; use crate::debounce::DebouncePolicy; use crate::platform::macos::events::Event; use crate::platform::macos::launching::Shortcut; use crate::platform::macos::launching::global_handler; +use crate::platform::macos::screen_with_mouse; use crate::platform::macos::{start_at_login, stop_at_login}; use crate::quit::get_open_apps; use crate::unit_conversion; @@ -114,6 +118,19 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { tile.focused = true; tile.visible = true; + let app = NSApplication::sharedApplication(MainThreadMarker::new().unwrap()); + + if let Some(window) = app.keyWindow() + && tile.config.window_location != Position::Default + { + let size = window.frame().size; + window.setFrameOrigin(tile.config.window_location.point( + size.width, + size.height, + screen_with_mouse(), + )); + }; + if tile.page == Page::Main && tile.query_lc.is_empty() { window::latest() .map(|x| x.unwrap()) @@ -496,12 +513,12 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { ]) } Message::RunFunction(command) => { - if let Function::TileWindow(pos) = &command { - if let Some(pid) = tile.frontmost.as_ref().map(|a| a.processIdentifier()) { - let ok = crate::platform::macos::window::tile_focused_window(pid, pos); - if !ok && tile.config.haptic_feedback { - perform_haptic(HapticPattern::Alignment); - } + if let Function::TileWindow(pos) = &command + && let Some(pid) = tile.frontmost.as_ref().map(|a| a.processIdentifier()) + { + let ok = crate::platform::macos::window::tile_focused_window(pid, pos); + if !ok && tile.config.haptic_feedback { + perform_haptic(HapticPattern::Alignment); } } command.execute(&tile.config); @@ -948,6 +965,9 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { SetConfigFields::SetBufferFields(SetConfigBufferFields::ClearOnHide(clear)) => { final_config.buffer_rules.clear_on_hide = clear; } + SetConfigFields::SetPosition(pos) => { + final_config.window_location = pos; + } SetConfigFields::SetBufferFields(SetConfigBufferFields::ClearOnEnter(clear)) => { final_config.buffer_rules.clear_on_enter = clear } diff --git a/src/config.rs b/src/config.rs index 1f74021..bd92447 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,6 +2,9 @@ use std::{collections::HashMap, path::Path, sync::Arc}; use iced::{Font, font::Family, theme::Custom, widget::image::Handle}; +use objc2::rc::Retained; +use objc2_app_kit::NSScreen; +use objc2_core_foundation::CGPoint; use serde::{Deserialize, Serialize}; use crate::{ @@ -13,6 +16,79 @@ use crate::{ utils::handle_from_icns, }; +#[derive(Debug, Clone, Deserialize, Serialize, Default, Copy, PartialEq)] +pub enum Position { + #[default] + Default, + TopCenter, + TopRight, + TopLeft, + MiddleCenter, + MiddleRight, + MiddleLeft, + BottomCenter, + BottomRight, + BottomLeft, +} + +impl Position { + pub fn variants() -> Vec { + vec![ + Position::Default, + Position::TopCenter, + Position::TopRight, + Position::TopLeft, + Position::MiddleCenter, + Position::MiddleRight, + Position::MiddleLeft, + Position::BottomCenter, + Position::BottomRight, + Position::BottomLeft, + ] + } + + pub fn point(&self, width: f64, height: f64, screen: Retained) -> CGPoint { + let frame = screen.frame(); + let ox = frame.origin.x; + let oy = frame.origin.y; + let sw = frame.size.width; + let sh = frame.size.height; + + match self { + Position::Default => CGPoint::new(ox, oy), + Position::TopLeft => CGPoint::new(ox, oy + sh - height), + Position::TopCenter => CGPoint::new(ox + (sw / 2.) - (width / 2.), oy + sh - height), + Position::TopRight => CGPoint::new(ox + sw - width, oy + sh - height), + Position::MiddleLeft => CGPoint::new(ox, oy + (sh / 2.) - (height / 2.)), + Position::MiddleCenter => CGPoint::new( + ox + (sw / 2.) - (width / 2.), + oy + (sh / 2.) - (height / 2.), + ), + Position::MiddleRight => CGPoint::new(ox + sw - width, oy + (sh / 2.) - (height / 2.)), + Position::BottomLeft => CGPoint::new(ox, oy), + Position::BottomCenter => CGPoint::new(ox + (sw / 2.) - (width / 2.), oy), + Position::BottomRight => CGPoint::new(ox + sw - width, oy), + } + } +} + +impl std::fmt::Display for Position { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Position::Default => write!(f, "Default"), + Position::TopCenter => write!(f, "Top Center"), + Position::TopRight => write!(f, "Top Right"), + Position::TopLeft => write!(f, "Top Left"), + Position::MiddleCenter => write!(f, "Middle Center"), + Position::MiddleRight => write!(f, "Middle Right"), + Position::MiddleLeft => write!(f, "Middle Left"), + Position::BottomCenter => write!(f, "Bottom Center"), + Position::BottomRight => write!(f, "Bottom Right"), + Position::BottomLeft => write!(f, "Bottom Left"), + } + } +} + /// The main config struct (effectively the config file's "schema") #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] #[serde(default)] @@ -24,6 +100,7 @@ pub struct Config { pub main_page: MainPage, pub start_at_login: bool, pub theme: Theme, + pub window_location: Position, pub placeholder: String, pub search_url: String, pub haptic_feedback: bool, @@ -56,6 +133,7 @@ impl Default for Config { haptic_feedback: false, auto_update: true, show_trayicon: true, + window_location: Position::Default, main_page: MainPage::default(), search_dirs: vec!["~".to_string()], log_path: "/tmp/rustcast.log".to_string(), @@ -91,18 +169,14 @@ impl std::fmt::Display for MainPage { /// The mode for the theme (dark, light, or follow system) #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Copy)] #[serde(rename_all = "lowercase")] +#[derive(Default)] pub enum ThemeMode { + #[default] Dark, Light, System, } -impl Default for ThemeMode { - fn default() -> Self { - ThemeMode::Dark - } -} - impl ThemeMode { /// Return preset text and background colors for this mode. pub fn presets( diff --git a/src/platform/macos/launching.rs b/src/platform/macos/launching.rs index 25dcabf..68e1a6e 100644 --- a/src/platform/macos/launching.rs +++ b/src/platform/macos/launching.rs @@ -121,7 +121,7 @@ extern "C-unwind" fn keyboard_event_callback( _ => return unsafe { event.as_mut() }, }; - if !data.targets.iter().any(|t| *t == shortcut) { + if !data.targets.contains(&shortcut) { return unsafe { event.as_mut() }; } diff --git a/src/platform/macos/mod.rs b/src/platform/macos/mod.rs index 9eeca17..c851b4a 100644 --- a/src/platform/macos/mod.rs +++ b/src/platform/macos/mod.rs @@ -8,6 +8,11 @@ pub mod urlscheme; pub mod window; use iced::wgpu::rwh::WindowHandle; +use objc2::Message; +use objc2::rc::Retained; +use objc2_app_kit::{NSEvent, NSScreen}; + +use crate::config::Position; pub(super) use self::discovery::get_installed_apps; pub(super) use self::haptics::perform_haptic; @@ -48,8 +53,29 @@ pub(super) fn set_activation_policy_accessory() { app.setActivationPolicy(NSApplicationActivationPolicy::Accessory); } +pub fn screen_with_mouse() -> Retained { + use objc2::MainThreadMarker; + + let mtm = MainThreadMarker::new().expect("must be on main thread"); + let mouse = NSEvent::mouseLocation(); + let screens = NSScreen::screens(mtm); + + screens + .iter() + .find(|s| { + let f = s.frame(); + mouse.x >= f.origin.x + && mouse.x < f.origin.x + f.size.width + && mouse.y >= f.origin.y + && mouse.y < f.origin.y + f.size.height + }) + .map(|s| s.retain()) + .or_else(|| NSScreen::mainScreen(mtm)) + .expect("no screens found") +} + /// This carries out the window configuration for the macos window (only things that are macos specific) -pub(super) fn macos_window_config(handle: &WindowHandle) { +pub(super) fn macos_window_config(handle: &WindowHandle, _position: Position) { use iced::wgpu::rwh::RawWindowHandle; use objc2::rc::Retained; use objc2_app_kit::NSView; diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 25a390c..910a3e5 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -2,7 +2,7 @@ use iced::wgpu::rwh::WindowHandle; pub use self::cross::default_app_paths; -use crate::app::apps::App; +use crate::{app::apps::App, config::Position}; pub mod cross; #[cfg(target_os = "macos")] @@ -13,9 +13,9 @@ pub fn set_activation_policy_accessory() { self::macos::set_activation_policy_accessory(); } -pub fn window_config(handle: &WindowHandle) { +pub fn window_config(handle: &WindowHandle, position: Position) { #[cfg(target_os = "macos")] - self::macos::macos_window_config(handle); + self::macos::macos_window_config(handle, position); } pub fn focus_this_app() { diff --git a/src/styles.rs b/src/styles.rs index e4b8f01..4bc964d 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -2,8 +2,9 @@ use crate::config::Theme as ConfigTheme; use iced::Shadow; use iced::border::Radius; +use iced::overlay::menu::{self}; use iced::widget::toggler::Status; -use iced::widget::{button, container, radio, scrollable, slider, toggler}; +use iced::widget::{button, container, pick_list, radio, scrollable, slider, toggler}; use iced::{Background, Border, Color, widget::text_input}; /// Helper: mix base color with white (simple “tint”) @@ -39,6 +40,35 @@ pub fn rustcast_text_input_style(theme: &ConfigTheme) -> text_input::Style { } } +pub fn picklist_style(theme: &ConfigTheme, _: pick_list::Status) -> pick_list::Style { + pick_list::Style { + text_color: (theme.text_color(1.)), + placeholder_color: with_alpha(theme.bg_color(), 1.), + handle_color: with_alpha(theme.bg_color(), 1.), + background: Background::Color(with_alpha(theme.bg_color(), 1.)), + border: Border { + color: theme.text_color(0.3), + width: 0.5, + radius: Radius::new(10), + }, + } +} + +pub fn picklist_menu_style(theme: &ConfigTheme) -> menu::Style { + menu::Style { + background: Background::Color(with_alpha(theme.bg_color(), 1.)), + border: Border { + color: theme.text_color(0.3), + width: 0.5, + radius: Radius::new(10), + }, + text_color: theme.text_color(0.5), + selected_text_color: theme.text_color(1.0), + selected_background: Background::Color(tint(with_alpha(theme.bg_color(), 1.), 0.1)), + shadow: Shadow::default(), + } +} + /// Container styling for all the elements in the rustcast window pub fn contents_style(theme: &ConfigTheme) -> container::Style { container::Style {