diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs index e0e98ec..d09375e 100644 --- a/examples/cosmic/src/window.rs +++ b/examples/cosmic/src/window.rs @@ -469,7 +469,6 @@ impl Application for Window { }; let mut header = header_bar() - .window_id(window::Id::MAIN) .title("COSMIC Design System - Iced") .on_close(Message::Close) .on_drag(Message::Drag) diff --git a/examples/multi-window/src/window.rs b/examples/multi-window/src/window.rs index dd47212..3fb843b 100644 --- a/examples/multi-window/src/window.rs +++ b/examples/multi-window/src/window.rs @@ -131,7 +131,11 @@ impl cosmic::Application for MultiWindow { let input = text_input("something", &w.input_value) .on_input(move |msg| Message::Input(input_id.clone(), msg)) .id(w.input_id.clone()); - + let focused = self + .core() + .focused_window() + .map(|i| i == id) + .unwrap_or_default(); let new_window_button = button(text("New Window")).on_press(Message::NewWindow); let content = scrollable( @@ -151,7 +155,7 @@ impl cosmic::Application for MultiWindow { if id == window::Id::MAIN { window_content.into() } else { - column![header_bar().window_id(id), window_content].into() + column![header_bar().focused(focused), window_content].into() } } diff --git a/src/app/core.rs b/src/app/core.rs index f40ae38..f0d3338 100644 --- a/src/app/core.rs +++ b/src/app/core.rs @@ -6,6 +6,7 @@ use std::collections::HashMap; use crate::config::CosmicTk; use cosmic_config::CosmicConfigEntry; use cosmic_theme::ThemeMode; +use iced::window; use iced_core::window::Id; use palette::Srgba; @@ -57,6 +58,9 @@ pub struct Core { /// Scaling factor used by the application scale_factor: f32, + /// Window focus state + pub(super) focused_window: Option, + pub(super) theme_sub_counter: u64, /// Last known system theme pub(super) system_theme: Theme, @@ -136,6 +140,7 @@ impl Default for Core { height: 0, width: 0, }, + focused_window: None, #[cfg(feature = "applet")] applet: crate::applet::Context::default(), #[cfg(feature = "single-instance")] @@ -285,6 +290,12 @@ impl Core { ) } + /// Get the current focused window if it exists + #[must_use] + pub fn focused_window(&self) -> Option { + self.focused_window.clone() + } + /// Whether the application should use a dark theme, according to the system #[must_use] pub fn system_is_dark(&self) -> bool { diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 45833ba..09c2d8c 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -78,6 +78,10 @@ pub enum Message { ShowWindowMenu, #[cfg(feature = "xdg-portal")] DesktopSettings(crate::theme::portal::Desktop), + /// Window focus changed + Focus(window::Id), + /// Window focus lost + Unfocus(window::Id), } #[derive(Default)] @@ -159,6 +163,10 @@ where iced::Event::Window(id, window::Event::Closed) => { return Some(Message::SurfaceClosed(id)) } + iced::Event::Window(id, window::Event::Focused) => return Some(Message::Focus(id)), + iced::Event::Window(id, window::Event::Unfocused) => { + return Some(Message::Unfocus(id)) + } #[cfg(feature = "wayland")] iced::Event::PlatformSpecific(PlatformSpecific::Wayland(event)) => match event { wayland::Event::Window(WindowEvent::State(state), _surface, id) => { @@ -557,6 +565,22 @@ impl Cosmic { Message::ToolkitConfig(config) => { self.app.core_mut().toolkit_config = config; } + + Message::Focus(f) => { + self.app.core_mut().focused_window = Some(f); + } + + Message::Unfocus(id) => { + let core = self.app.core_mut(); + if core + .focused_window + .as_ref() + .map(|cur| *cur == id) + .unwrap_or_default() + { + core.focused_window = None; + } + } } iced::Command::none() diff --git a/src/app/mod.rs b/src/app/mod.rs index d7fd322..3e01539 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -642,6 +642,10 @@ impl ApplicationExt for App { fn view_main(&self) -> Element> { let core = self.core(); let is_condensed = core.is_condensed(); + let focused = core + .focused_window() + .map(|i| i == self.main_window_id()) + .unwrap_or_default(); let content_row = crate::widget::row::with_children({ let mut widgets = Vec::with_capacity(2); @@ -686,7 +690,7 @@ impl ApplicationExt for App { .push_maybe(if core.window.show_headerbar { Some({ let mut header = crate::widget::header_bar() - .window_id(self.main_window_id()) + .focused(focused) .title(&core.window.header_title) .on_drag(Message::Cosmic(cosmic::Message::Drag)) .on_close(Message::Cosmic(cosmic::Message::Close)) diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs index 4b3549c..afd9dff 100644 --- a/src/theme/style/button.rs +++ b/src/theme/style/button.rs @@ -22,6 +22,7 @@ pub enum Button { Destructive, Link, Icon, + HeaderBar, IconVertical, Image, #[default] @@ -66,7 +67,7 @@ pub fn appearance( appearance.icon_color = icon; } - Button::Icon | Button::IconVertical => { + Button::Icon | Button::IconVertical | Button::HeaderBar => { if matches!(style, Button::IconVertical) { corner_radii = &cosmic.corner_radii.radius_m; } @@ -146,14 +147,23 @@ pub fn appearance( impl StyleSheet for crate::Theme { type Style = Button; - fn active(&self, focused: bool, style: &Self::Style) -> Appearance { + fn active(&self, focused: bool, selected: bool, style: &Self::Style) -> Appearance { if let Button::Custom { active, .. } = style { return active(focused, self); } + let accent = self.cosmic().accent_color(); appearance(self, focused, style, |component| { - let text_color = if let Button::Icon | Button::IconVertical = style { - None + let text_color = if matches!( + style, + Button::Icon | Button::IconVertical | Button::HeaderBar + ) && selected + { + Some(accent.into()) + } else if matches!(style, Button::HeaderBar) && !selected { + let mut c = Color::from(component.on); + c.a = 0.8; + Some(c) } else { Some(component.on.into()) }; @@ -179,39 +189,61 @@ impl StyleSheet for crate::Theme { } fn drop_target(&self, style: &Self::Style) -> Appearance { - self.active(false, style) + self.active(false, false, style) } - fn hovered(&self, focused: bool, style: &Self::Style) -> Appearance { + fn hovered(&self, focused: bool, selected: bool, style: &Self::Style) -> Appearance { if let Button::Custom { hovered, .. } = style { return hovered(focused, self); } + let accent = self.cosmic().accent_button.hover; appearance( self, focused || matches!(style, Button::Image), style, |component| { - ( - component.hover.into(), - Some(component.on.into()), - Some(component.on.into()), - ) + let text_color = if matches!( + style, + Button::Icon | Button::IconVertical | Button::HeaderBar + ) && selected + { + Some(accent.into()) + } else if matches!(style, Button::HeaderBar) && !selected { + let mut c = Color::from(component.on); + c.a = 0.8; + Some(c) + } else { + Some(component.on.into()) + }; + + (component.hover.into(), text_color, text_color) }, ) } - fn pressed(&self, focused: bool, style: &Self::Style) -> Appearance { + fn pressed(&self, focused: bool, selected: bool, style: &Self::Style) -> Appearance { if let Button::Custom { pressed, .. } = style { return pressed(focused, self); } + let accent = self.cosmic().accent_button.pressed; appearance(self, focused, style, |component| { - ( - component.pressed.into(), - Some(component.on.into()), - Some(component.on.into()), - ) + let text_color = if matches!( + style, + Button::Icon | Button::IconVertical | Button::HeaderBar + ) && selected + { + Some(accent.into()) + } else if matches!(style, Button::HeaderBar) && !selected { + let mut c = Color::from(component.on); + c.a = 0.8; + Some(c) + } else { + Some(component.on.into()) + }; + + (component.pressed.into(), text_color, text_color) }) } diff --git a/src/widget/button/icon.rs b/src/widget/button/icon.rs index 9a70a51..6ac9838 100644 --- a/src/widget/button/icon.rs +++ b/src/widget/button/icon.rs @@ -16,12 +16,14 @@ pub type Button<'a, Message> = Builder<'a, Message, Icon>; pub struct Icon { handle: Handle, vertical: bool, + selected: bool, } pub fn icon<'a, Message>(handle: impl Into) -> Button<'a, Message> { Button::new(Icon { handle: handle.into(), vertical: false, + selected: false, }) } @@ -124,6 +126,11 @@ impl<'a, Message> Button<'a, Message> { self } + pub fn selected(mut self, selected: bool) -> Self { + self.variant.selected = selected; + self + } + pub fn vertical(mut self, vertical: bool) -> Self { self.variant.vertical = vertical; self.style = Style::IconVertical; @@ -179,6 +186,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes .padding(0) .id(builder.id) .on_press_maybe(builder.on_press) + .selected(builder.variant.selected) .style(builder.style); if builder.tooltip.is_empty() { diff --git a/src/widget/button/style.rs b/src/widget/button/style.rs index 9cda41e..a097c3a 100644 --- a/src/widget/button/style.rs +++ b/src/widget/button/style.rs @@ -68,21 +68,21 @@ pub trait StyleSheet { type Style: Default; /// Produces the active [`Appearance`] of a button. - fn active(&self, focused: bool, style: &Self::Style) -> Appearance; + fn active(&self, focused: bool, selected: bool, style: &Self::Style) -> Appearance; /// Produces the disabled [`Appearance`] of a button. fn disabled(&self, style: &Self::Style) -> Appearance; /// [`Appearance`] when the button is the target of a DND operation. fn drop_target(&self, style: &Self::Style) -> Appearance { - self.hovered(false, style) + self.hovered(false, false, style) } /// Produces the hovered [`Appearance`] of a button. - fn hovered(&self, focused: bool, style: &Self::Style) -> Appearance; + fn hovered(&self, focused: bool, selected: bool, style: &Self::Style) -> Appearance; /// Produces the pressed [`Appearance`] of a button. - fn pressed(&self, focused: bool, style: &Self::Style) -> Appearance; + fn pressed(&self, focused: bool, selected: bool, style: &Self::Style) -> Appearance; /// Background color of the selection indicator fn selection_background(&self) -> Background; diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index 033fe24..d17dd62 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -528,10 +528,10 @@ where } if self.on_press.is_none() { - node.set_disabled() + node.set_disabled(); } if is_hovered { - node.set_hovered() + node.set_hovered(); } node.set_default_action_verb(DefaultActionVerb::Click); @@ -696,12 +696,12 @@ where style_sheet.disabled(style) } else if is_mouse_over { if state.is_pressed { - style_sheet.pressed(state.is_focused || is_selected, style) + style_sheet.pressed(state.is_focused, is_selected, style) } else { - style_sheet.hovered(state.is_focused || is_selected, style) + style_sheet.hovered(state.is_focused, is_selected, style) } } else { - style_sheet.active(state.is_focused || is_selected, style) + style_sheet.active(state.is_focused, is_selected, style) }; let doubled_border_width = styling.border_width * 2.0; diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index 0c4dc0b..7ff646b 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -799,7 +799,7 @@ pub fn color_button<'a, Message: 'static>( } else { (0.0, Color::TRANSPARENT) }; - let standard = theme.active(focused, &Button::Standard); + let standard = theme.active(focused, false, &Button::Standard); button::Appearance { shadow_offset: Vector::default(), background: color.map(Background::from).or(standard.background), @@ -837,7 +837,7 @@ pub fn color_button<'a, Message: 'static>( (0.0, Color::TRANSPARENT) }; - let standard = theme.hovered(focused, &Button::Standard); + let standard = theme.hovered(focused, false, &Button::Standard); button::Appearance { shadow_offset: Vector::default(), background: color.map(Background::from).or(standard.background), @@ -859,7 +859,7 @@ pub fn color_button<'a, Message: 'static>( (0.0, Color::TRANSPARENT) }; - let standard = theme.pressed(focused, &Button::Standard); + let standard = theme.pressed(focused, false, &Button::Standard); button::Appearance { shadow_offset: Vector::default(), background: color.map(Background::from).or(standard.background), diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 6bed982..60fcbc8 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -5,7 +5,7 @@ use crate::{ext::CollectionWidget, widget, Element}; use apply::Apply; use derive_setters::Setters; use iced::{window, Length}; -use iced_core::{renderer::Quad, widget::tree, Background, Renderer, Widget}; +use iced_core::{widget::tree, Widget}; use std::borrow::Cow; #[must_use] @@ -20,7 +20,7 @@ pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { start: Vec::new(), center: Vec::new(), end: Vec::new(), - window_id: None, + focused: false, } } @@ -50,9 +50,8 @@ pub struct HeaderBar<'a, Message> { #[setters(strip_option)] on_right_click: Option, - /// The window id for the headerbar. - #[setters(strip_option)] - window_id: Option, + /// Focused state of the window + focused: bool, /// Elements packed at the start of the headerbar. #[setters(skip)] @@ -100,7 +99,6 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { #[must_use] pub fn build(self) -> HeaderBarWidget<'a, Message> { HeaderBarWidget { - window_id: self.window_id, header_bar_inner: self.into_element(), } } @@ -108,7 +106,6 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { pub struct HeaderBarWidget<'a, Message> { header_bar_inner: Element<'a, Message>, - window_id: Option, } impl<'a, Message: Clone + 'static> Widget @@ -122,23 +119,6 @@ impl<'a, Message: Clone + 'static> Widget tree::Tag { - tree::Tag::of::() - } - - fn diff(&mut self, tree: &mut tree::Tree) { - tree.diff_children(&mut [&mut self.header_bar_inner]); - let prev = tree.state.downcast_mut::(); - if prev.window_id != self.window_id { - *prev = MyState::new(self.window_id); - } - } - - fn state(&self) -> tree::State { - let state = MyState::new(self.window_id); - tree::State::new(state) - } - fn layout( &self, tree: &mut tree::Tree, @@ -174,28 +154,6 @@ impl<'a, Message: Clone + 'static> Widget(); - if !state.window_has_focus { - let header_bar_appearance = - ::appearance( - theme, - &crate::theme::Container::HeaderBar, - ); - let cosmic = theme.cosmic(); - let mut neutral_0 = cosmic.palette.neutral_0; - neutral_0.alpha = 0.3; - - // draw overlay rectangle - renderer.fill_quad( - Quad { - bounds: layout.bounds(), - border: header_bar_appearance.border, - shadow: header_bar_appearance.shadow, - }, - Background::Color(neutral_0.into()), - ); - } } fn on_event( @@ -209,14 +167,6 @@ impl<'a, Message: Clone + 'static> Widget, viewport: &iced_core::Rectangle, ) -> iced_core::event::Status { - if let iced_core::Event::Window(id, iced_core::window::Event::Focused) = event { - let state = state.state.downcast_mut::(); - state.focus_window(id); - } else if let iced_core::Event::Window(id, iced_core::window::Event::Unfocused) = event { - let state = state.state.downcast_mut::(); - state.unfocus_window(id); - } - let child_state = &mut state.children[0]; let child_layout = layout.children().next().unwrap(); self.header_bar_inner.as_widget_mut().on_event( @@ -370,6 +320,8 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { widget::icon::from_svg_bytes(icon_bytes) .symbolic(true) .apply(widget::button::icon) + .style(crate::theme::Button::HeaderBar) + .selected(self.focused) .icon_size(size) .on_press(on_press) }; @@ -415,31 +367,3 @@ impl<'a, Message: Clone + 'static> From> for Elemen Element::new(headerbar) } } - -pub struct MyState { - pub window_id: Option, - pub window_has_focus: bool, -} - -impl MyState { - pub fn new(id: Option) -> Self { - Self { - window_id: id, - window_has_focus: id.is_none(), - } - } - - pub fn focus_window(&mut self, id: window::Id) { - if self.window_id != Some(id) { - return; - } - self.window_has_focus = true; - } - - pub fn unfocus_window(&mut self, id: window::Id) { - if self.window_id != Some(id) { - return; - } - self.window_has_focus = false; - } -}