diff --git a/core/src/interp/spice/format.rs b/core/src/interp/spice/format.rs index 6ed8804..3099d62 100644 --- a/core/src/interp/spice/format.rs +++ b/core/src/interp/spice/format.rs @@ -10,13 +10,13 @@ pub(crate) fn format_spice(n: f64, unit: &str) -> String { let (prefix, scale): (&str, f64) = if abs_n >= 1.0 { ("", 1.0) } else if abs_n >= 1e-3 { - ("M", 1e-3) + ("m", 1e-3) } else if abs_n >= 1e-6 { ("U", 1e-6) } else if abs_n >= 1e-9 { ("N", 1e-9) } else { - ("P", 1e-12) + ("p", 1e-12) }; let mantissa = n / scale; let mag = mantissa.abs().log10().floor() as i32; diff --git a/core/src/interp/spice/literal.rs b/core/src/interp/spice/literal.rs index c8229c9..ac8485c 100644 --- a/core/src/interp/spice/literal.rs +++ b/core/src/interp/spice/literal.rs @@ -2,10 +2,15 @@ pub(crate) const SPICE_UNITS: &[&str] = &["F", "H", "HZ", "V", "A", "W", "R", "O pub(crate) fn spice_prefix_scale(c: char) -> Option { match c { - 'm' | 'M' => Some(1e-3), + 'P' => Some(1e15), + 't' | 'T' => Some(1e12), + 'b' | 'B' => Some(1e9), + 'M' => Some(1e6), + 'k' | 'K' => Some(1e3), + 'm' => Some(1e-3), 'u' | 'U' | 'µ' | 'μ' => Some(1e-6), 'n' | 'N' => Some(1e-9), - 'p' | 'P' => Some(1e-12), + 'p' => Some(1e-12), _ => None, } } diff --git a/core/src/interp/tests/spice.rs b/core/src/interp/tests/spice.rs index e1bd27f..2702cc8 100644 --- a/core/src/interp/tests/spice.rs +++ b/core/src/interp/tests/spice.rs @@ -87,7 +87,28 @@ use super::helpers::*; let mut i = Interpreter::new(); i.exec("use spice").unwrap(); let v = i.eval("0.5nF").unwrap(); - assert_eq!(v.display(), "500PF"); + assert_eq!(v.display(), "500pF"); + } + + #[test] + fn spice_large_prefixes_case_sensitive() { + let mut i = Interpreter::new(); + i.exec("use spice").unwrap(); + let mut scale = |src: &str| match i.eval(src).unwrap() { + Value::Spice(n, _) => n, + Value::Number(n) => n, + other => panic!("expected scalar, got {:?}", other), + }; + assert!(approx(scale("100kHz"), 1e5)); + assert!(approx(scale("100KHz"), 1e5)); + assert!(approx(scale("100Mhz"), 1e8)); + assert!(approx(scale("100mhz"), 0.1)); + assert!(approx(scale("2bHz"), 2e9)); + assert!(approx(scale("2BHz"), 2e9)); + assert!(approx(scale("3THz"), 3e12)); + assert!(approx(scale("3tHz"), 3e12)); + assert!(approx(scale("5PHz"), 5e15)); + assert!(approx(scale("5pHz"), 5e-12)); } #[test] diff --git a/viewport/src/editor/free_layer.rs b/viewport/src/editor/free_layer.rs index 2d962aa..e72a7d0 100644 --- a/viewport/src/editor/free_layer.rs +++ b/viewport/src/editor/free_layer.rs @@ -397,7 +397,7 @@ impl super::EditorState { let Some(inner) = inner_opt else { continue }; let is_active = self.active_free.as_ref() == Some(id); - let band = (self.font_size * 1.3) * 0.5; + let band = (self.font_size * 1.3).max(18.0); let body: Element<'_, Message, Theme, iced_wgpu::Renderer> = if is_active { let right = self.build_resize_band( id.clone(), diff --git a/viewport/src/editor/mod.rs b/viewport/src/editor/mod.rs index 565c808..27f3e88 100644 --- a/viewport/src/editor/mod.rs +++ b/viewport/src/editor/mod.rs @@ -85,12 +85,18 @@ impl EditorState { .into() } else { let editor = self.view_blocks(); - match self.build_free_overlay() { - Some(overlay) => iced_widget::stack![editor, overlay] - .width(Length::Fill) - .height(Length::Fill) - .into(), - None => editor, + let mut layers: Vec> = vec![editor]; + if let Some(overlay) = self.build_free_overlay() { + layers.push(overlay); + } + // context menu renders above the free overlay + if let Some(menu_state) = &self.context_menu { + layers.push(self.context_menu_view(menu_state)); + } + if layers.len() == 1 { + layers.pop().unwrap() + } else { + iced_widget::stack(layers).width(Length::Fill).height(Length::Fill).into() } }; @@ -167,12 +173,9 @@ impl EditorState { } fn view_blocks(&self) -> Element<'_, Message, Theme, iced_wgpu::Renderer> { - let has_computed_layers = !self.eval_results.is_empty() - || !self.computed_tables.is_empty() - || !self.computed_trees.is_empty(); + // a lone text block always renders through the compositor let single_text_block = self.block_count() == 1 - && self.block_at(0).map(|b| b.as_any().is::()).unwrap_or(false) - && !has_computed_layers; + && self.block_at(0).map(|b| b.as_any().is::()).unwrap_or(false); #[cfg(any(target_os = "linux", target_os = "windows"))] let title_bar_h = 0.0_f32; @@ -379,15 +382,11 @@ impl EditorState { }; let minimap_layer = self.minimap_overlay().unwrap_or_else(empty_layer); - let ctx_layer = match &self.context_menu { - Some(menu_state) => self.context_menu_view(menu_state), - None => empty_layer(), - }; let spillover_layer = self.spillover_view().unwrap_or_else(empty_layer); // the document scrollable stays at stack layer 0, never re-wrapped by an overlay. // a Fill stack gives the scrollable the viewport height, not the zero intrinsic of a Shrink stack - iced_widget::stack![inner, selection_tint, minimap_layer, ctx_layer, spillover_layer] + iced_widget::stack![inner, selection_tint, minimap_layer, spillover_layer] .width(Length::Fill) .height(Length::Fill) .into() @@ -867,7 +866,7 @@ impl EditorState { CtxTarget::Object(node_id) => { let placed = self.free_placements.contains_key(node_id); let mode = self.object_mode(node_id); - // a bulleted radio row that sets this object's mode + // one object-mode option, bulleted when active let mode_item = |label: &str, m: LayoutMode| -> Element<'_, Message, Theme, iced_wgpu::Renderer> { item( &format!("{} {}", if mode == m { "•" } else { " " }, label),