diff --git a/src/app/config.rs b/src/app/config.rs index 68e329c44..01e6354eb 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -32,6 +32,9 @@ pub struct AppConfig { pub dock: crate::ui::dock::DockState, /// Whether the optional Graphic Attributes palette is visible. pub show_graphic_attributes: bool, + /// Graphic Attributes creates fills on the current layer instead of the + /// layer of the filled object. + pub graphic_fills_on_current_layer: bool, /// Add a newly selected annotation scale to existing annotative objects. pub annotation_auto_scale: i8, /// Ribbon collapse density. @@ -55,6 +58,7 @@ impl Default for AppConfig { statusbar: StatusBarConfig::default(), dock: crate::ui::dock::DockState::default(), show_graphic_attributes: false, + graphic_fills_on_current_layer: false, annotation_auto_scale: -4, ribbon: RibbonConfig::default(), plot: PlotDialogState::default(), diff --git a/src/app/update/file.rs b/src/app/update/file.rs index ab8db72ea..22171ca59 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -1087,6 +1087,7 @@ impl OpenCADStudio { dock }, show_graphic_attributes: self.show_graphic_attributes, + graphic_fills_on_current_layer: self.graphic_attributes.fill_on_current_layer, annotation_auto_scale: self.annotation_auto_scale, ribbon: crate::app::config::RibbonConfig { collapse: self.ribbon.collapse_mode(), @@ -1146,6 +1147,10 @@ impl OpenCADStudio { dock.ensure_settings(); self.dock = dock; self.show_graphic_attributes = cfg.show_graphic_attributes; + self.graphic_attributes.fill_on_current_layer = cfg.graphic_fills_on_current_layer; + if self.show_graphic_attributes { + self.dock.dock_below_properties(crate::ui::dock::PanelId::GraphicAttributes); + } self.ribbon .set_graphic_attributes(self.show_graphic_attributes); self.annotation_auto_scale = cfg.annotation_auto_scale.clamp(-4, 4); diff --git a/src/app/update/graphic_attributes.rs b/src/app/update/graphic_attributes.rs index 52fb76ace..36dc86b5a 100644 --- a/src/app/update/graphic_attributes.rs +++ b/src/app/update/graphic_attributes.rs @@ -233,6 +233,11 @@ impl OpenCADStudio { M::SetAllByLayer => self.on_graphic_attributes_set_all(true), M::SetAllByBlock => self.on_graphic_attributes_set_all(false), M::RemoveReferences => self.on_graphic_attributes_remove_references(), + M::ToggleFillOnCurrentLayer => { + self.graphic_attributes.fill_on_current_layer ^= true; + self.save_config(); + Task::none() + } M::CreateLayer => { use crate::command::CadCommand; let command = crate::command::FreeTextValuePromptCommand::new( @@ -668,11 +673,12 @@ impl OpenCADStudio { .iter() .copied() .filter(|handle| { - self.tabs[i] - .scene - .document - .get_entity(*handle) - .is_some_and(is_open_fill_boundary) + !self.tabs[i].scene.is_layer_locked(*handle) + && self.tabs[i] + .scene + .document + .get_entity(*handle) + .is_some_and(is_open_fill_boundary) }) .collect(); if !open_boundaries.is_empty() { @@ -712,6 +718,19 @@ impl OpenCADStudio { selected: &[Handle], to_close: &[Handle], ) -> Task { + let (selected, locked): (Vec, Vec) = selected + .iter() + .partition(|handle| !self.tabs[i].scene.is_layer_locked(**handle)); + if !locked.is_empty() { + self.command_line.push_info( + crate::tf!( + "Graphic Attributes: {} object(s) on a locked layer were not changed.", + locked.len() + ) + .as_ref(), + ); + } + let selected = selected.as_slice(); let pending = self.begin_undo(i, "Graphic Attributes", selected.len(), true); let mut changed = false; for handle in to_close { @@ -809,7 +828,12 @@ impl OpenCADStudio { .with_origin(origin) .with_pattern(name.clone(), pattern.clone()); if let crate::command::CmdResult::CommitHatch(hatch) = command.finish_selected() { - new_hatches.push(hatch); + let layer = if self.graphic_attributes.fill_on_current_layer { + self.tabs[i].active_layer.clone() + } else { + entity.common().layer.clone() + }; + new_hatches.push((hatch, layer)); changed_boundaries.insert(*handle); } else if !is_open_fill_boundary(entity) { // Open boundaries the user chose not to close are expected @@ -861,8 +885,7 @@ impl OpenCADStudio { for entity in updated_hatches { self.tabs[i].scene.update_entity(entity); } - let layer = self.tabs[i].active_layer.clone(); - for hatch in new_hatches { + for (hatch, layer) in new_hatches { self.tabs[i].scene.add_hatch(hatch, Some(&layer), None); } true @@ -1690,4 +1713,107 @@ mod tests { assert!(!app.ribbon.show_graphic_attributes); assert!(!app.current_config().show_graphic_attributes); } + + fn closed_square_on(app: &mut OpenCADStudio, layer: &str) -> Handle { + let i = app.active_tab; + if app.tabs[i].scene.document.layers.get(layer).is_none() { + let mut new_layer = acadrust::tables::Layer::new(layer); + new_layer.handle = app.tabs[i].scene.document.allocate_handle(); + let _ = app.tabs[i].scene.document.layers.add(new_layer); + } + let handle = open_square(app); + if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) { + if let EntityType::LwPolyline(polyline) = entity { + polyline.is_closed = true; + } + entity.common_mut().layer = layer.to_string(); + } + handle + } + + fn fill_layers(app: &OpenCADStudio) -> Vec { + app.tabs[app.active_tab] + .scene + .document + .entities() + .filter_map(|entity| match entity { + EntityType::Hatch(hatch) => Some(hatch.common.layer.clone()), + _ => None, + }) + .collect() + } + + #[test] + fn new_fills_follow_the_object_layer_unless_the_menu_option_is_set() { + let mut app = fresh(); + closed_square_on(&mut app, "Walls"); + let _ = app.update(ga(GraphicAttributesMsg::Fill(GraphicAttribute::Solid))); + assert_eq!(fill_layers(&app), vec!["Walls".to_string()]); + + let mut app = fresh(); + let current = app.tabs[app.active_tab].active_layer.clone(); + closed_square_on(&mut app, "Walls"); + let _ = app.update(ga(GraphicAttributesMsg::ToggleFillOnCurrentLayer)); + assert!(app.current_config().graphic_fills_on_current_layer); + let _ = app.update(ga(GraphicAttributesMsg::Fill(GraphicAttribute::Solid))); + assert_eq!(fill_layers(&app), vec![current]); + } + + #[test] + fn objects_on_locked_layers_get_no_fill() { + let mut app = fresh(); + let i = app.active_tab; + closed_square_on(&mut app, "Locked"); + closed_square_on(&mut app, "Open"); + app.tabs[i] + .scene + .document + .layers + .get_mut("Locked") + .expect("locked layer") + .lock(); + let _ = app.update(ga(GraphicAttributesMsg::Fill(GraphicAttribute::Solid))); + assert_eq!(fill_layers(&app), vec!["Open".to_string()]); + } + + #[test] + fn palette_fill_lookup_follows_document_changes() { + let mut app = fresh(); + let i = app.active_tab; + let boundary = closed_square_on(&mut app, "0"); + let selected = vec![boundary]; + let (fills, kind) = app.graphic_attributes.fills(&app.tabs[i].scene, &selected); + assert!(fills.is_empty()); + assert_eq!(kind, GraphicAttribute::None); + + let _ = app.update(ga(GraphicAttributesMsg::Fill(GraphicAttribute::Solid))); + let (fills, kind) = app.graphic_attributes.fills(&app.tabs[i].scene, &selected); + assert_eq!(fills.len(), 1); + assert_eq!(kind, GraphicAttribute::Solid); + } + + #[test] + fn showing_the_palette_docks_it_below_properties_once() { + use crate::app::config::DockSide; + use crate::ui::dock::PanelId; + let mut app = OpenCADStudio::new_for_test(); + app.apply_config(crate::app::config::AppConfig::default()); + assert_eq!(app.dock.location(PanelId::GraphicAttributes), None); + + let _ = app.update(Message::ToggleGraphicAttributes); + let properties = app.dock.location(PanelId::Properties).expect("properties docked"); + assert_eq!( + app.dock.location(PanelId::GraphicAttributes), + Some((properties.0, properties.1 + 1)) + ); + + // A place the user chose is kept when the palette is shown again. + app.dock.dock(PanelId::GraphicAttributes, DockSide::Right, 0); + let _ = app.update(Message::ToggleGraphicAttributes); + let _ = app.update(Message::ToggleGraphicAttributes); + assert_eq!( + app.dock.location(PanelId::GraphicAttributes), + Some((DockSide::Right, 0)) + ); + } } diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 82adcf37a..2d682e884 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -4161,7 +4161,9 @@ impl OpenCADStudio { Message::ToggleGraphicAttributes => { self.show_graphic_attributes ^= true; self.ribbon.set_graphic_attributes(self.show_graphic_attributes); - if !self.show_graphic_attributes { + if self.show_graphic_attributes { + self.dock.dock_below_properties(crate::ui::dock::PanelId::GraphicAttributes); + } else { self.graphic_attributes.close_popups(); } self.save_config(); diff --git a/src/ui/dock.rs b/src/ui/dock.rs index caf96dd62..975bd5e58 100644 --- a/src/ui/dock.rs +++ b/src/ui/dock.rs @@ -123,7 +123,7 @@ pub struct DockState { impl Default for DockState { fn default() -> Self { Self { - left: vec![PanelId::Properties, PanelId::GraphicAttributes], + left: vec![PanelId::Properties], right: vec![PanelId::BlockPalette], panels: BTreeMap::new(), } @@ -159,13 +159,18 @@ impl DockState { ] { self.panels.entry(id).or_insert_with(|| DockPanel::for_id(id)); } - if self.location(PanelId::GraphicAttributes).is_none() { - if let Some((side, index)) = self.location(PanelId::Properties) { - self.stack_mut(side).insert(index + 1, PanelId::GraphicAttributes); - } else { - self.left.push(PanelId::GraphicAttributes); - } + } + + /// Dock `id` directly below Properties (or at the end of the left edge) + /// unless it already has a place, which is then kept. + pub fn dock_below_properties(&mut self, id: PanelId) { + if self.location(id).is_some() { + return; } + let (side, index) = self + .location(PanelId::Properties) + .map_or((DockSide::Left, usize::MAX), |(side, index)| (side, index + 1)); + self.dock(id, side, index); } /// Where (if anywhere) a panel is currently docked. @@ -273,10 +278,6 @@ mod tests { fn default_docks_each_known_panel_on_an_edge() { let state = DockState::default(); assert_eq!(state.location(PanelId::Properties), Some((DockSide::Left, 0))); - assert_eq!( - state.location(PanelId::GraphicAttributes), - Some((DockSide::Left, 1)) - ); assert_eq!( state.location(PanelId::BlockPalette), Some((DockSide::Right, 0)) @@ -302,7 +303,7 @@ mod tests { Some((DockSide::Right, 0)) ); // It no longer occupies the left edge. - assert_eq!(state.left, vec![PanelId::GraphicAttributes]); + assert!(state.left.is_empty()); assert_eq!(state.right.len(), 2); } diff --git a/src/ui/window/gradient_editor.rs b/src/ui/window/gradient_editor.rs index 30d6925bc..a51976f1d 100644 --- a/src/ui/window/gradient_editor.rs +++ b/src/ui/window/gradient_editor.rs @@ -27,8 +27,12 @@ pub struct GradientEditorState { pub inverted: bool, pub angle: String, pub centered: bool, + /// Large preview, rebuilt only when a setting it depends on changes. + preview: std::cell::RefCell)>>, } +type PreviewKey = (GradientColorMode, AcadColor, AcadColor, u32, GradientKind, bool, String, bool); + #[derive(Debug, Clone)] pub enum GradientMsg { Open, @@ -77,6 +81,7 @@ impl GradientEditorState { inverted, angle: format!("{:.1}°", hatch.gradient_color.angle.to_degrees()), centered: hatch.gradient_color.shift.abs() < 1e-9, + preview: Default::default(), } } @@ -191,18 +196,30 @@ fn preview_sized( } fn preview(state: &GradientEditorState) -> Element<'static, Message> { - preview_image(preview_sized(state, 370, 80)) + let key: PreviewKey = ( + state.color_mode, + state.color_1, + state.color_2, + state.shade_tint.to_bits(), + state.kind, + state.inverted, + state.angle.clone(), + state.centered, + ); + let mut cache = state.preview.borrow_mut(); + if cache.as_ref().is_none_or(|(cached, _)| *cached != key) { + *cache = Some((key, preview_sized(state, 370, 80))); + } + preview_image(cache.as_ref().and_then(|(_, handle)| handle.clone())) } -pub(crate) fn compact_preview(hatch: &acadrust::entities::Hatch) -> Element<'static, Message> { - preview_image(preview_sized( - &GradientEditorState::from_hatch(Vec::new(), hatch), - 256, - 26, - )) +pub(crate) fn compact_preview( + hatch: &acadrust::entities::Hatch, +) -> Option { + preview_sized(&GradientEditorState::from_hatch(Vec::new(), hatch), 256, 26) } -fn preview_image(handle: Option) -> Element<'static, Message> { +pub(crate) fn preview_image(handle: Option) -> Element<'static, Message> { match handle { Some(handle) => image(handle) .width(Fill) diff --git a/src/ui/window/graphic_attributes.rs b/src/ui/window/graphic_attributes.rs index e1a50bc55..5d327e91b 100644 --- a/src/ui/window/graphic_attributes.rs +++ b/src/ui/window/graphic_attributes.rs @@ -6,15 +6,16 @@ use crate::app::{ColorPickTarget, Message}; use crate::scene::Scene; use crate::ui::dock::{DockMsg, PanelId}; use crate::ui::window::gradient_editor::{GradientEditorState, GradientMsg}; -use acadrust::entities::{EntityCommon, Hatch}; +use acadrust::entities::{EntityCommon, Hatch, HatchGradientPattern}; use acadrust::types::{Color as AcadColor, LineWeight, Transparency}; use acadrust::{CadDocument, EntityType, Handle}; use iced::widget::{ - button, column, combo_box, container, mouse_area, row, slider, text, text_input, tooltip, + button, image, column, combo_box, container, mouse_area, row, slider, text, text_input, tooltip, Space, }; use iced::{Background, Border, Element, Fill, Length, Padding, Theme}; use rustc_hash::{FxHashMap, FxHashSet}; +use std::cell::RefCell; type DrawDepth = FxHashMap; @@ -101,6 +102,8 @@ pub enum GraphicAttributesMsg { SetAllByBlock, RemoveReferences, CreateLayer, + /// Put new fills on the current layer instead of their object's layer. + ToggleFillOnCurrentLayer, Fill(GraphicAttribute), LineColor(AcadColor), LineLinetype(String), @@ -145,6 +148,12 @@ pub struct GraphicAttributesState { pub hatch_editor: Option, pub gradient_editor: Option, pub pending_fill_close: Option, + /// New fills go to the current layer instead of their object's layer. + pub fill_on_current_layer: bool, + /// View-side caches; `view` runs after every message, so neither the + /// fill lookup nor the gradient preview is rebuilt unless its input changed. + fill_index: RefCell>, + gradient_preview: RefCell)>>, } impl GraphicAttributesState { @@ -154,6 +163,36 @@ impl GraphicAttributesState { self.gradient_editor = None; } + /// The fills and fill kind of `selected`, from an index of associative + /// fills that is rebuilt only when the scene's geometry changes. + pub(crate) fn fills(&self, scene: &Scene, selected: &[Handle]) -> (Vec, GraphicAttribute) { + let key = (std::ptr::from_ref(scene) as usize, scene.geometry_epoch); + let mut cache = self.fill_index.borrow_mut(); + if cache.as_ref().is_none_or(|(cached, _)| *cached != key) { + *cache = Some((key, FillIndex::build(&scene.document))); + } + let index = &cache.as_ref().expect("fill index was just built").1; + ( + index.fill_handles(&scene.document, selected), + index.current(&scene.document, selected), + ) + } + + fn gradient_preview(&self, hatch: &Hatch) -> Element<'static, Message> { + let mut cache = self.gradient_preview.borrow_mut(); + if cache + .as_ref() + .is_none_or(|(gradient, _)| *gradient != hatch.gradient_color) + { + *cache = Some(( + hatch.gradient_color.clone(), + crate::ui::window::gradient_editor::compact_preview(hatch), + )); + } + let handle = cache.as_ref().and_then(|(_, handle)| handle.clone()); + crate::ui::window::gradient_editor::preview_image(handle) + } + /// Whether a flyout or question is bound to the selection it was opened for. pub fn tracks_selection(&self) -> bool { self.hatch_editor.is_some() @@ -190,62 +229,91 @@ impl GraphicAttributesState { } } -/// The fill kind of each selected object: its own kind for a selected HATCH, -/// otherwise the kind of the associative HATCH it bounds. -pub(crate) fn current(document: &CadDocument, selected: &[Handle]) -> GraphicAttribute { - // Scan the document once, rather than once per selected object on every frame. - let mut fills: FxHashMap> = - selected.iter().map(|handle| (*handle, None)).collect(); - for entity in document.entities() { - let EntityType::Hatch(hatch) = entity else { - continue; - }; - let value = GraphicAttribute::of(hatch); - if let Some(fill) = fills.get_mut(&entity.common().handle) { - fill.get_or_insert(value); - } - if !hatch.is_associative { - continue; - } - for handle in hatch.paths.iter().flat_map(|path| &path.boundary_handles) { - if let Some(fill) = fills.get_mut(handle) { - fill.get_or_insert(value); +/// Associative HATCH entities per boundary object, in document order. +#[derive(Debug, Default)] +pub(crate) struct FillIndex { + by_boundary: FxHashMap>, +} + +impl FillIndex { + pub(crate) fn build(document: &CadDocument) -> Self { + let mut by_boundary: FxHashMap> = FxHashMap::default(); + for entity in document.entities() { + let EntityType::Hatch(hatch) = entity else { + continue; + }; + if !hatch.is_associative { + continue; + } + for boundary in hatch.paths.iter().flat_map(|path| &path.boundary_handles) { + let fills = by_boundary.entry(*boundary).or_default(); + if !fills.contains(&hatch.common.handle) { + fills.push(hatch.common.handle); + } } } + Self { by_boundary } } - let Some((&first, rest)) = selected.split_first() else { - return GraphicAttribute::None; - }; - let first = fills[&first].unwrap_or(GraphicAttribute::None); - if rest - .iter() - .any(|handle| fills[handle].unwrap_or(GraphicAttribute::None) != first) - { - GraphicAttribute::Varies - } else { - first + + /// The fill kind of each selected object: its own kind for a selected + /// HATCH, otherwise the kind of the first associative HATCH it bounds. + pub(crate) fn current(&self, document: &CadDocument, selected: &[Handle]) -> GraphicAttribute { + let fill_of = |handle: &Handle| { + let own = match document.get_entity(*handle) { + Some(EntityType::Hatch(hatch)) => return GraphicAttribute::of(hatch), + _ => None, + }; + own.or_else(|| { + self.by_boundary.get(handle)?.iter().find_map(|fill| { + match document.get_entity(*fill)? { + EntityType::Hatch(hatch) => Some(GraphicAttribute::of(hatch)), + _ => None, + } + }) + }) + .unwrap_or(GraphicAttribute::None) + }; + let Some((first, rest)) = selected.split_first() else { + return GraphicAttribute::None; + }; + let first = fill_of(first); + if rest.iter().any(|handle| fill_of(handle) != first) { + GraphicAttribute::Varies + } else { + first + } + } + + /// HATCH entities that are selected or associated with a selected boundary. + pub(crate) fn fill_handles(&self, document: &CadDocument, selected: &[Handle]) -> Vec { + let mut seen = FxHashSet::default(); + let mut fills = Vec::new(); + for handle in selected { + if matches!(document.get_entity(*handle), Some(EntityType::Hatch(_))) + && seen.insert(*handle) + { + fills.push(*handle); + } + for fill in self.by_boundary.get(handle).into_iter().flatten() { + if document.get_entity(*fill).is_some() && seen.insert(*fill) { + fills.push(*fill); + } + } + } + fills } } +/// The fill kind of the selection; see [`FillIndex::current`]. +#[cfg(test)] +pub(crate) fn current(document: &CadDocument, selected: &[Handle]) -> GraphicAttribute { + FillIndex::build(document).current(document, selected) +} + /// HATCH entities that are selected or associated with a selected boundary. +/// Scans the whole drawing; the palette view uses its cached [`FillIndex`]. pub(crate) fn fill_handles(document: &CadDocument, selected: &[Handle]) -> Vec { - let selected: FxHashSet<_> = selected.iter().copied().collect(); - document - .entities() - .filter_map(|entity| { - let EntityType::Hatch(hatch) = entity else { - return None; - }; - let directly_selected = selected.contains(&entity.common().handle); - let selected_boundary = hatch.is_associative - && hatch - .paths - .iter() - .flat_map(|path| &path.boundary_handles) - .any(|handle| selected.contains(handle)); - (directly_selected || selected_boundary).then_some(entity.common().handle) - }) - .collect() + FillIndex::build(document).fill_handles(document, selected) } /// Selected non-HATCH objects plus the boundaries of selected associative @@ -717,7 +785,7 @@ fn transparency_control<'a>( row![control, more].spacing(4).align_y(iced::Center).into() } -fn header_menu(side: DockSide, open: bool) -> Element<'static, Message> { +fn header_menu(side: DockSide, open: bool, fill_on_current_layer: bool) -> Element<'static, Message> { let menu_button = button(crate::ui::icons::themed_secondary( crate::ui::icons::MENU, 12.0, @@ -747,6 +815,14 @@ fn header_menu(side: DockSide, open: bool) -> Element<'static, Message> { item("Set all attributes ByBlock", GraphicAttributesMsg::SetAllByBlock), item("Remove ByLayer / ByBlock", GraphicAttributesMsg::RemoveReferences), item("Create Layer with active settings", GraphicAttributesMsg::CreateLayer), + container( + iced::widget::checkbox(fill_on_current_layer) + .label("Create fills on the current layer") + .on_toggle(|_| msg(GraphicAttributesMsg::ToggleFillOnCurrentLayer)) + .size(12) + .text_size(11), + ) + .padding([4, 8]), ]) .style(crate::ui::color_select::popup_panel_style) .padding(2); @@ -759,7 +835,12 @@ fn header_menu(side: DockSide, open: bool) -> Element<'static, Message> { .into() } -fn header<'a>(side: DockSide, auto_collapse: bool, menu_open: bool) -> Element<'a, Message> { +fn header<'a>( + side: DockSide, + auto_collapse: bool, + menu_open: bool, + fill_on_current_layer: bool, +) -> Element<'a, Message> { let pin_icon = if auto_collapse { crate::ui::icons::themed_primary_weak_text(crate::ui::icons::PIN, 12.0) } else { @@ -785,7 +866,7 @@ fn header<'a>(side: DockSide, auto_collapse: bool, menu_open: bool) -> Element<' row![ text(crate::t!("Graphic Attributes")).size(12), Space::new().width(Fill), - header_menu(side, menu_open), + header_menu(side, menu_open, fill_on_current_layer), pin, close ] @@ -1202,7 +1283,7 @@ pub fn view<'a>( let draw_depth = draw_depth.as_ref(); let open_menu = state.open_menu; let line_handles = line_handles(document, &selected); - let fill_handles = fill_handles(document, &selected); + let (fill_handles, current) = state.fills(scene, &selected); let mut body = column![text(crate::t!("Line")).size(11)].spacing(6); body = body.extend(line_section( @@ -1215,7 +1296,6 @@ pub fn view<'a>( open_menu, )); - let current = current(document, &selected); body = body .push(Space::new().height(2)) .push(text(crate::t!("Fill")).size(11)) @@ -1279,7 +1359,7 @@ pub fn view<'a>( } GraphicAttribute::Gradient => { if let Some(hatch) = top_fill_hatch(document, &fill_handles, draw_depth, current) { - let preview = crate::ui::window::gradient_editor::compact_preview(hatch); + let preview = state.gradient_preview(hatch); let preview = if gradients_vary(document, &fill_handles) { iced::widget::stack![ preview, @@ -1331,7 +1411,12 @@ pub fn view<'a>( } container(column![ - header(side, auto_collapse, open_menu == Some(Menu::Header)), + header( + side, + auto_collapse, + open_menu == Some(Menu::Header), + state.fill_on_current_layer + ), body.padding(8) ]) .width(Length::Fixed(width))