1use crate::cosmic_theme::{Density, Spacing};
5use crate::{Element, theme, widget};
6use apply::Apply;
7use derive_setters::Setters;
8use iced::Length;
9use iced_core::{Vector, Widget, widget::tree};
10use std::{borrow::Cow, cmp};
11
12#[must_use]
13pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> {
14 HeaderBar {
15 title: Cow::Borrowed(""),
16 on_close: None,
17 on_drag: None,
18 on_maximize: None,
19 on_minimize: None,
20 on_right_click: None,
21 start: Vec::new(),
22 center: Vec::new(),
23 end: Vec::new(),
24 density: None,
25 focused: false,
26 maximized: false,
27 sharp_corners: false,
28 is_ssd: false,
29 on_double_click: None,
30 is_condensed: false,
31 }
32}
33
34#[derive(Setters)]
35pub struct HeaderBar<'a, Message> {
36 #[setters(skip)]
38 title: Cow<'a, str>,
39
40 #[setters(strip_option)]
42 on_close: Option<Message>,
43
44 #[setters(strip_option)]
46 on_drag: Option<Message>,
47
48 #[setters(strip_option)]
50 on_maximize: Option<Message>,
51
52 #[setters(strip_option)]
54 on_minimize: Option<Message>,
55
56 #[setters(strip_option)]
59 on_double_click: Option<Message>,
60
61 #[setters(strip_option)]
63 on_right_click: Option<Message>,
64
65 #[setters(skip)]
67 start: Vec<Element<'a, Message>>,
68
69 #[setters(skip)]
71 center: Vec<Element<'a, Message>>,
72
73 #[setters(skip)]
75 end: Vec<Element<'a, Message>>,
76
77 #[setters(strip_option)]
79 density: Option<Density>,
80
81 focused: bool,
83
84 maximized: bool,
86
87 sharp_corners: bool,
89
90 is_ssd: bool,
92
93 is_condensed: bool,
95}
96
97impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
98 #[must_use]
100 pub fn title(mut self, title: impl Into<Cow<'a, str>> + 'a) -> Self {
101 self.title = title.into();
102 self
103 }
104
105 #[must_use]
107 pub fn start(mut self, widget: impl Into<Element<'a, Message>> + 'a) -> Self {
108 self.start.push(widget.into());
109 self
110 }
111
112 #[must_use]
114 pub fn center(mut self, widget: impl Into<Element<'a, Message>> + 'a) -> Self {
115 self.center.push(widget.into());
116 self
117 }
118
119 #[must_use]
121 pub fn end(mut self, widget: impl Into<Element<'a, Message>> + 'a) -> Self {
122 self.end.push(widget.into());
123 self
124 }
125
126 #[must_use]
128 #[inline]
129 pub fn build(self) -> HeaderBarWidget<'a, Message> {
130 HeaderBarWidget {
131 header_bar_inner: self.view(),
132 }
133 }
134}
135
136pub struct HeaderBarWidget<'a, Message> {
137 header_bar_inner: Element<'a, Message>,
138}
139
140impl<Message: Clone + 'static> Widget<Message, crate::Theme, crate::Renderer>
141 for HeaderBarWidget<'_, Message>
142{
143 fn diff(&mut self, tree: &mut tree::Tree) {
144 tree.diff_children(&mut [&mut self.header_bar_inner]);
145 }
146
147 fn children(&self) -> Vec<tree::Tree> {
148 vec![tree::Tree::new(&self.header_bar_inner)]
149 }
150
151 fn size(&self) -> iced_core::Size<Length> {
152 self.header_bar_inner.as_widget().size()
153 }
154
155 fn layout(
156 &self,
157 tree: &mut tree::Tree,
158 renderer: &crate::Renderer,
159 limits: &iced_core::layout::Limits,
160 ) -> iced_core::layout::Node {
161 let child_tree = &mut tree.children[0];
162 let child = self
163 .header_bar_inner
164 .as_widget()
165 .layout(child_tree, renderer, limits);
166 iced_core::layout::Node::with_children(child.size(), vec![child])
167 }
168
169 fn draw(
170 &self,
171 tree: &tree::Tree,
172 renderer: &mut crate::Renderer,
173 theme: &crate::Theme,
174 style: &iced_core::renderer::Style,
175 layout: iced_core::Layout<'_>,
176 cursor: iced_core::mouse::Cursor,
177 viewport: &iced_core::Rectangle,
178 ) {
179 let layout_children = layout.children().next().unwrap();
180 let state_children = &tree.children[0];
181 self.header_bar_inner.as_widget().draw(
182 state_children,
183 renderer,
184 theme,
185 style,
186 layout_children,
187 cursor,
188 viewport,
189 );
190 }
191
192 fn on_event(
193 &mut self,
194 state: &mut tree::Tree,
195 event: iced_core::Event,
196 layout: iced_core::Layout<'_>,
197 cursor: iced_core::mouse::Cursor,
198 renderer: &crate::Renderer,
199 clipboard: &mut dyn iced_core::Clipboard,
200 shell: &mut iced_core::Shell<'_, Message>,
201 viewport: &iced_core::Rectangle,
202 ) -> iced_core::event::Status {
203 let child_state = &mut state.children[0];
204 let child_layout = layout.children().next().unwrap();
205 self.header_bar_inner.as_widget_mut().on_event(
206 child_state,
207 event,
208 child_layout,
209 cursor,
210 renderer,
211 clipboard,
212 shell,
213 viewport,
214 )
215 }
216
217 fn mouse_interaction(
218 &self,
219 state: &tree::Tree,
220 layout: iced_core::Layout<'_>,
221 cursor: iced_core::mouse::Cursor,
222 viewport: &iced_core::Rectangle,
223 renderer: &crate::Renderer,
224 ) -> iced_core::mouse::Interaction {
225 let child_tree = &state.children[0];
226 let child_layout = layout.children().next().unwrap();
227 self.header_bar_inner.as_widget().mouse_interaction(
228 child_tree,
229 child_layout,
230 cursor,
231 viewport,
232 renderer,
233 )
234 }
235
236 fn operate(
237 &self,
238 state: &mut tree::Tree,
239 layout: iced_core::Layout<'_>,
240 renderer: &crate::Renderer,
241 operation: &mut dyn iced_core::widget::Operation<()>,
242 ) {
243 let child_tree = &mut state.children[0];
244 let child_layout = layout.children().next().unwrap();
245 self.header_bar_inner
246 .as_widget()
247 .operate(child_tree, child_layout, renderer, operation);
248 }
249
250 fn overlay<'b>(
251 &'b mut self,
252 state: &'b mut tree::Tree,
253 layout: iced_core::Layout<'_>,
254 renderer: &crate::Renderer,
255 translation: Vector,
256 ) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
257 let child_tree = &mut state.children[0];
258 let child_layout = layout.children().next().unwrap();
259 self.header_bar_inner.as_widget_mut().overlay(
260 child_tree,
261 child_layout,
262 renderer,
263 translation,
264 )
265 }
266
267 fn drag_destinations(
268 &self,
269 state: &tree::Tree,
270 layout: iced_core::Layout<'_>,
271 renderer: &crate::Renderer,
272 dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
273 ) {
274 if let Some((child_tree, child_layout)) =
275 state.children.iter().zip(layout.children()).next()
276 {
277 self.header_bar_inner.as_widget().drag_destinations(
278 child_tree,
279 child_layout,
280 renderer,
281 dnd_rectangles,
282 );
283 }
284 }
285
286 #[cfg(feature = "a11y")]
287 fn a11y_nodes(
289 &self,
290 layout: iced_core::Layout<'_>,
291 state: &tree::Tree,
292 p: iced::mouse::Cursor,
293 ) -> iced_accessibility::A11yTree {
294 let c_layout = layout.children().next().unwrap();
295 let c_state = &state.children[0];
296 self.header_bar_inner
297 .as_widget()
298 .a11y_nodes(c_layout, c_state, p)
299 }
300}
301
302impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
303 #[allow(clippy::too_many_lines)]
304 pub fn view(mut self) -> Element<'a, Message> {
306 let Spacing {
307 space_xxxs,
308 space_xxs,
309 ..
310 } = theme::spacing();
311
312 let start = std::mem::take(&mut self.start);
314 let center = std::mem::take(&mut self.center);
315 let mut end = std::mem::take(&mut self.end);
316
317 let window_control_cnt = self.on_close.is_some() as usize
318 + self.on_maximize.is_some() as usize
319 + self.on_minimize.is_some() as usize;
320 end.push(self.window_controls());
322
323 let padding = match self.density.unwrap_or_else(crate::config::header_size) {
325 Density::Compact => {
326 if self.maximized {
327 [4, 8, 4, 8]
328 } else {
329 [3, 7, 4, 7]
330 }
331 }
332 _ => {
333 if self.maximized {
334 [8, 8, 8, 8]
335 } else {
336 [7, 7, 8, 7]
337 }
338 }
339 };
340
341 let acc_count = |v: &[Element<'a, Message>]| {
342 v.iter().fold(0, |acc, e| {
343 acc + match e.as_widget().size().width {
344 Length::Fixed(w) if w > 30. => (w / 30.0).ceil() as usize,
345 _ => 1,
346 }
347 })
348 };
349
350 let left_len = acc_count(&start);
351 let right_len = acc_count(&end);
352
353 let portion = ((left_len.max(right_len + window_control_cnt) as f32
354 / center.len().max(1) as f32)
355 .round() as u16)
356 .max(1);
357 let (left_portion, right_portion) =
358 if center.is_empty() && (self.title.is_empty() || self.is_condensed) {
359 let left_to_right_ratio = left_len as f32 / right_len.max(1) as f32;
360 let right_to_left_ratio = right_len as f32 / left_len.max(1) as f32;
361 if right_to_left_ratio > 2. || left_len < 1 {
362 (1, 2)
363 } else if left_to_right_ratio > 2. || right_len < 1 {
364 (2, 1)
365 } else {
366 (left_len as u16, (right_len + window_control_cnt) as u16)
367 }
368 } else {
369 (portion, portion)
370 };
371 let title_portion = cmp::max(left_portion, right_portion) * 2;
372 let mut widget = widget::row::with_capacity(3)
374 .push(
376 widget::row::with_children(start)
377 .spacing(space_xxxs)
378 .align_y(iced::Alignment::Center)
379 .apply(widget::container)
380 .align_x(iced::Alignment::Start)
381 .width(Length::FillPortion(left_portion)),
382 )
383 .push_maybe(if !center.is_empty() {
386 Some(
387 widget::row::with_children(center)
388 .spacing(space_xxxs)
389 .align_y(iced::Alignment::Center)
390 .apply(widget::container)
391 .center_x(Length::Fill)
392 .into(),
393 )
394 } else if !self.title.is_empty() && !self.is_condensed {
395 Some(self.title_widget(title_portion))
396 } else {
397 None
398 })
399 .push(
400 widget::row::with_children(end)
401 .spacing(space_xxs)
402 .align_y(iced::Alignment::Center)
403 .apply(widget::container)
404 .align_x(iced::Alignment::End)
405 .width(Length::FillPortion(right_portion)),
406 )
407 .align_y(iced::Alignment::Center)
408 .height(Length::Fixed(32.0 + padding[0] as f32 + padding[2] as f32))
409 .padding(if self.is_ssd { [0, 8, 0, 8] } else { padding })
410 .spacing(8)
411 .apply(widget::container)
412 .class(crate::theme::Container::HeaderBar {
413 focused: self.focused,
414 sharp_corners: self.sharp_corners,
415 })
416 .center_y(Length::Shrink)
417 .apply(widget::mouse_area);
418
419 if let Some(message) = self.on_drag.clone() {
421 widget = widget.on_drag(message);
422 }
423
424 if let Some(message) = self.on_maximize.clone() {
426 widget = widget.on_release(message);
427 }
428 if let Some(message) = self.on_double_click.clone() {
429 widget = widget.on_double_press(message);
430 }
431 if let Some(message) = self.on_right_click.clone() {
432 widget = widget.on_right_press(message);
433 }
434
435 widget.into()
436 }
437
438 fn title_widget(&mut self, title_portion: u16) -> Element<'a, Message> {
439 let mut title = Cow::default();
440 std::mem::swap(&mut title, &mut self.title);
441
442 widget::text::heading(title)
443 .apply(widget::container)
444 .center(Length::FillPortion(title_portion))
445 .into()
446 }
447
448 fn window_controls(&mut self) -> Element<'a, Message> {
450 macro_rules! icon {
451 ($name:expr, $size:expr, $on_press:expr) => {{
452 #[cfg(target_os = "linux")]
453 let icon = {
454 widget::icon::from_name($name)
455 .apply(widget::button::icon)
456 .padding(8)
457 };
458
459 #[cfg(not(target_os = "linux"))]
460 let icon = {
461 widget::icon::from_svg_bytes(include_bytes!(concat!(
462 "../../res/icons/",
463 $name,
464 ".svg"
465 )))
466 .symbolic(true)
467 .apply(widget::button::icon)
468 .padding(8)
469 };
470
471 icon.class(crate::theme::Button::HeaderBar)
472 .selected(self.focused)
473 .icon_size($size)
474 .on_press($on_press)
475 }};
476 }
477
478 widget::row::with_capacity(3)
479 .push_maybe(
480 self.on_minimize
481 .take()
482 .map(|m: Message| icon!("window-minimize-symbolic", 16, m)),
483 )
484 .push_maybe(self.on_maximize.take().map(|m| {
485 if self.maximized {
486 icon!("window-restore-symbolic", 16, m)
487 } else {
488 icon!("window-maximize-symbolic", 16, m)
489 }
490 }))
491 .push_maybe(
492 self.on_close
493 .take()
494 .map(|m| icon!("window-close-symbolic", 16, m)),
495 )
496 .spacing(theme::spacing().space_xxs)
497 .apply(widget::container)
498 .center_y(Length::Fill)
499 .into()
500 }
501}
502
503impl<'a, Message: Clone + 'static> From<HeaderBar<'a, Message>> for Element<'a, Message> {
504 fn from(headerbar: HeaderBar<'a, Message>) -> Self {
505 Element::new(headerbar.build())
506 }
507}
508
509impl<'a, Message: Clone + 'static> From<HeaderBarWidget<'a, Message>> for Element<'a, Message> {
510 fn from(headerbar: HeaderBarWidget<'a, Message>) -> Self {
511 Element::new(headerbar)
512 }
513}