forked from 1b-agent/opencadstudio-upstream
perf(ribbon): cache natural panel measurements
This commit is contained in:
1 parent
42ba7f12a0
commit
140303217f
2 files changed
+352
-60
No files matched your search
+95
-38
@@ -8,12 +8,12 @@
|
||||
//! panel, so it shrinks as the panels do.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
|
||||
use iced::advanced::layout::{self, Layout};
|
||||
use iced::advanced::widget::{self, Widget};
|
||||
use iced::advanced::{mouse, overlay, renderer, Renderer as _, Shell};
|
||||
use iced::advanced::{Renderer as _, Shell, mouse, overlay, renderer};
|
||||
use iced::{
|
||||
Background, Border, Element, Event, Length, Point, Rectangle, Renderer, Shadow, Size, Theme,
|
||||
Vector,
|
||||
@@ -173,6 +173,23 @@ pub(crate) struct CollapsePanels<'a> {
|
||||
/// User-chosen density. `Auto` runs the width-based degradation; the others
|
||||
/// pin every panel to one level regardless of the window width.
|
||||
mode: CollapseMode,
|
||||
/// Fingerprint of content that can affect natural panel dimensions.
|
||||
measure_key: u64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MeasureCache {
|
||||
key: Option<u64>,
|
||||
widths: Vec<Widths>,
|
||||
heights: Vec<[f32; 4]>,
|
||||
}
|
||||
|
||||
impl MeasureCache {
|
||||
fn matches(&self, key: u64, panel_count: usize) -> bool {
|
||||
self.key == Some(key)
|
||||
&& self.widths.len() == panel_count
|
||||
&& self.heights.len() == panel_count
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> CollapsePanels<'a> {
|
||||
@@ -186,9 +203,18 @@ impl<'a> CollapsePanels<'a> {
|
||||
height_out: None,
|
||||
tight_out: None,
|
||||
mode: CollapseMode::Auto,
|
||||
measure_key: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reuse natural measurements while the panel content and font metrics stay
|
||||
/// unchanged. The available window width is intentionally not part of the
|
||||
/// key: these measurements use unbounded natural limits.
|
||||
pub fn measure_key(mut self, key: u64) -> Self {
|
||||
self.measure_key = key;
|
||||
self
|
||||
}
|
||||
|
||||
/// Report the measured row height into `out` on every layout.
|
||||
pub fn report_height(mut self, out: Arc<AtomicU32>) -> Self {
|
||||
self.height_out = Some(out);
|
||||
@@ -225,6 +251,14 @@ impl<'a> CollapsePanels<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
|
||||
fn tag(&self) -> widget::tree::Tag {
|
||||
widget::tree::Tag::of::<MeasureCache>()
|
||||
}
|
||||
|
||||
fn state(&self) -> widget::tree::State {
|
||||
widget::tree::State::new(MeasureCache::default())
|
||||
}
|
||||
|
||||
fn diff(&mut self, tree: &mut widget::Tree) {
|
||||
let mut refs = Vec::with_capacity(self.panels.len() * SLOTS);
|
||||
for p in &mut self.panels {
|
||||
@@ -254,42 +288,52 @@ impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
|
||||
let mut heights = Vec::with_capacity(n);
|
||||
let auto = self.mode == CollapseMode::Auto;
|
||||
if auto {
|
||||
for i in 0..n {
|
||||
let f = self.panels[i].elements[Level::Full as usize]
|
||||
.as_widget_mut()
|
||||
.layout(&mut tree.children[slot(i, Level::Full)], renderer, &natural)
|
||||
.size();
|
||||
let c = self.panels[i].elements[Level::Compact as usize]
|
||||
.as_widget_mut()
|
||||
.layout(
|
||||
&mut tree.children[slot(i, Level::Compact)],
|
||||
renderer,
|
||||
&natural,
|
||||
)
|
||||
.size();
|
||||
let b = self.panels[i].elements[Level::Collapsed as usize]
|
||||
.as_widget_mut()
|
||||
.layout(
|
||||
&mut tree.children[slot(i, Level::Collapsed)],
|
||||
renderer,
|
||||
&natural,
|
||||
)
|
||||
.size();
|
||||
let t = self.panels[i].elements[Level::Tight as usize]
|
||||
.as_widget_mut()
|
||||
.layout(
|
||||
&mut tree.children[slot(i, Level::Tight)],
|
||||
renderer,
|
||||
&natural,
|
||||
)
|
||||
.size();
|
||||
widths.push(Widths {
|
||||
full: f.width,
|
||||
compact: c.width,
|
||||
button: b.width,
|
||||
tight: t.width,
|
||||
});
|
||||
heights.push([f.height, c.height, b.height, t.height]);
|
||||
let cache = tree.state.downcast_ref::<MeasureCache>();
|
||||
if cache.matches(self.measure_key, n) {
|
||||
widths.clone_from(&cache.widths);
|
||||
heights.clone_from(&cache.heights);
|
||||
} else {
|
||||
for i in 0..n {
|
||||
let f = self.panels[i].elements[Level::Full as usize]
|
||||
.as_widget_mut()
|
||||
.layout(&mut tree.children[slot(i, Level::Full)], renderer, &natural)
|
||||
.size();
|
||||
let c = self.panels[i].elements[Level::Compact as usize]
|
||||
.as_widget_mut()
|
||||
.layout(
|
||||
&mut tree.children[slot(i, Level::Compact)],
|
||||
renderer,
|
||||
&natural,
|
||||
)
|
||||
.size();
|
||||
let b = self.panels[i].elements[Level::Collapsed as usize]
|
||||
.as_widget_mut()
|
||||
.layout(
|
||||
&mut tree.children[slot(i, Level::Collapsed)],
|
||||
renderer,
|
||||
&natural,
|
||||
)
|
||||
.size();
|
||||
let t = self.panels[i].elements[Level::Tight as usize]
|
||||
.as_widget_mut()
|
||||
.layout(
|
||||
&mut tree.children[slot(i, Level::Tight)],
|
||||
renderer,
|
||||
&natural,
|
||||
)
|
||||
.size();
|
||||
widths.push(Widths {
|
||||
full: f.width,
|
||||
compact: c.width,
|
||||
button: b.width,
|
||||
tight: t.width,
|
||||
});
|
||||
heights.push([f.height, c.height, b.height, t.height]);
|
||||
}
|
||||
let cache = tree.state.downcast_mut::<MeasureCache>();
|
||||
cache.key = Some(self.measure_key);
|
||||
cache.widths.clone_from(&widths);
|
||||
cache.heights.clone_from(&heights);
|
||||
}
|
||||
}
|
||||
let levels = if let Some(level) = self.mode.forced_level() {
|
||||
@@ -696,6 +740,19 @@ impl overlay::Overlay<Message, Theme, Renderer> for FlyoutOverlay<'_, '_> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn natural_measurements_are_reused_only_for_the_same_key_and_panel_count() {
|
||||
let cache = MeasureCache {
|
||||
key: Some(17),
|
||||
widths: vec![Widths::default(); 2],
|
||||
heights: vec![[0.0; 4]; 2],
|
||||
};
|
||||
|
||||
assert!(cache.matches(17, 2));
|
||||
assert!(!cache.matches(18, 2));
|
||||
assert!(!cache.matches(17, 1));
|
||||
}
|
||||
|
||||
fn w(full: f32, compact: f32, button: f32, tight: f32) -> Widths {
|
||||
Widths {
|
||||
full,
|
||||
|
||||
+257
-22
@@ -6,8 +6,10 @@
|
||||
//
|
||||
// Dropdown items within a group are collected into columns of 3 rows.
|
||||
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
|
||||
use rustc_hash::FxHashMap as HashMap;
|
||||
|
||||
@@ -16,9 +18,9 @@ use iced::widget::{button, column, container, mouse_area, row, scrollable, text}
|
||||
use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::modules::{CadModule, IconKind, RibbonGroup, RibbonItem};
|
||||
use crate::modules::{CadModule, IconKind, RibbonGroup, RibbonItem, ToolDef};
|
||||
use crate::plugin::all_ribbon_modules;
|
||||
use crate::ui::properties::{linetype_display_name, lw_options, LinetypeItem};
|
||||
use crate::ui::properties::{LinetypeItem, linetype_display_name, lw_options};
|
||||
|
||||
mod draw_panel;
|
||||
pub(crate) mod widgets;
|
||||
@@ -27,9 +29,9 @@ mod color_dropdown;
|
||||
mod context_tools;
|
||||
mod modify_panel;
|
||||
mod visibility_panel;
|
||||
pub use context_tools::{pdf_underlay_tools, point_cloud_tools, xref_tools, UnderlayContext};
|
||||
use widgets::{StyleContext, *};
|
||||
pub use context_tools::{UnderlayContext, pdf_underlay_tools, point_cloud_tools, xref_tools};
|
||||
pub(crate) use widgets::{REDO_HISTORY_ID, UNDO_HISTORY_ID};
|
||||
use widgets::{StyleContext, *};
|
||||
mod collapse;
|
||||
mod keytips;
|
||||
mod locate;
|
||||
@@ -38,9 +40,8 @@ use crate::ui::wrap_bar::{PosReport, WrapBar, WrapFlow};
|
||||
pub use collapse::CollapseMode;
|
||||
use collapse::{CollapsePanels, Panel};
|
||||
pub use keytips::{
|
||||
active as keytips_active, key_pressed as keytip_key_pressed,
|
||||
key_released as keytip_key_released, pointer_pressed as keytip_pointer_pressed, KeyTipInput,
|
||||
KeyTipStep,
|
||||
KeyTipInput, KeyTipStep, active as keytips_active, key_pressed as keytip_key_pressed,
|
||||
key_released as keytip_key_released, pointer_pressed as keytip_pointer_pressed,
|
||||
};
|
||||
|
||||
pub(crate) fn tooltip_content(text: String) -> Element<'static, Message> {
|
||||
@@ -56,6 +57,144 @@ fn dropdown_header(id: &str) -> Option<&'static str> {
|
||||
(id == "IMPORT_FILE").then_some("Import File — the face runs the last one chosen")
|
||||
}
|
||||
|
||||
fn hash_tool_layout(tool: &ToolDef, hasher: &mut impl Hasher) {
|
||||
tool.id.hash(hasher);
|
||||
t!(tool.label).as_ref().hash(hasher);
|
||||
}
|
||||
|
||||
fn hash_item_layout(item: &RibbonItem, hasher: &mut impl Hasher) {
|
||||
match item {
|
||||
RibbonItem::Tool(tool) => {
|
||||
0u8.hash(hasher);
|
||||
hash_tool_layout(tool, hasher);
|
||||
}
|
||||
RibbonItem::LabeledTool(tool) => {
|
||||
1u8.hash(hasher);
|
||||
hash_tool_layout(tool, hasher);
|
||||
}
|
||||
RibbonItem::LargeTool(tool) => {
|
||||
2u8.hash(hasher);
|
||||
hash_tool_layout(tool, hasher);
|
||||
}
|
||||
RibbonItem::Dropdown { id, items, .. } => {
|
||||
3u8.hash(hasher);
|
||||
id.hash(hasher);
|
||||
items.len().hash(hasher);
|
||||
for (command, label, _) in items {
|
||||
command.hash(hasher);
|
||||
t!(*label).as_ref().hash(hasher);
|
||||
}
|
||||
}
|
||||
RibbonItem::LabeledDropdown {
|
||||
id, label, items, ..
|
||||
} => {
|
||||
4u8.hash(hasher);
|
||||
id.hash(hasher);
|
||||
t!(*label).as_ref().hash(hasher);
|
||||
items.len().hash(hasher);
|
||||
for (command, item_label, _) in items {
|
||||
command.hash(hasher);
|
||||
t!(*item_label).as_ref().hash(hasher);
|
||||
}
|
||||
}
|
||||
RibbonItem::LargeDropdown {
|
||||
id, label, items, ..
|
||||
} => {
|
||||
5u8.hash(hasher);
|
||||
id.hash(hasher);
|
||||
t!(*label).as_ref().hash(hasher);
|
||||
items.len().hash(hasher);
|
||||
for (command, item_label, _) in items {
|
||||
command.hash(hasher);
|
||||
t!(*item_label).as_ref().hash(hasher);
|
||||
}
|
||||
}
|
||||
RibbonItem::ToolGrid { columns } => {
|
||||
6u8.hash(hasher);
|
||||
columns.len().hash(hasher);
|
||||
for column in columns {
|
||||
column.len().hash(hasher);
|
||||
for tool in column {
|
||||
hash_tool_layout(tool, hasher);
|
||||
}
|
||||
}
|
||||
}
|
||||
RibbonItem::LayerComboGroup { row2, row3 } => {
|
||||
7u8.hash(hasher);
|
||||
row2.len().hash(hasher);
|
||||
row3.len().hash(hasher);
|
||||
for tool in row2.iter().chain(row3) {
|
||||
hash_tool_layout(tool, hasher);
|
||||
}
|
||||
}
|
||||
RibbonItem::PropertiesGroup { match_prop } => {
|
||||
8u8.hash(hasher);
|
||||
hash_tool_layout(match_prop, hasher);
|
||||
}
|
||||
RibbonItem::StyleComboGroup {
|
||||
style_key,
|
||||
combo_id,
|
||||
rows,
|
||||
..
|
||||
} => {
|
||||
9u8.hash(hasher);
|
||||
format!("{style_key:?}").hash(hasher);
|
||||
combo_id.hash(hasher);
|
||||
for row in rows {
|
||||
row.len().hash(hasher);
|
||||
for tool in row {
|
||||
hash_tool_layout(tool, hasher);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn panel_measure_key(
|
||||
groups: &[&RibbonGroup],
|
||||
last_cmd: &HashMap<&'static str, &'static str>,
|
||||
last_panel_tool: &HashMap<&'static str, &'static str>,
|
||||
style_ctx: &StyleContext<'_>,
|
||||
visibility: Option<&crate::ui::window::block_visibility::RibbonContext>,
|
||||
) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
crate::sys::ui_font::generation().hash(&mut hasher);
|
||||
groups.len().hash(&mut hasher);
|
||||
for group in groups {
|
||||
group.title.hash(&mut hasher);
|
||||
group.tools.len().hash(&mut hasher);
|
||||
for item in &group.tools {
|
||||
hash_item_layout(item, &mut hasher);
|
||||
}
|
||||
}
|
||||
let mut commands: Vec<_> = last_cmd.iter().collect();
|
||||
commands.sort_unstable_by_key(|(id, _)| **id);
|
||||
for (id, command) in commands {
|
||||
id.hash(&mut hasher);
|
||||
command.hash(&mut hasher);
|
||||
}
|
||||
let mut representatives: Vec<_> = last_panel_tool.iter().collect();
|
||||
representatives.sort_unstable_by_key(|(title, _)| **title);
|
||||
for (title, tool) in representatives {
|
||||
title.hash(&mut hasher);
|
||||
tool.hash(&mut hasher);
|
||||
}
|
||||
for (names, active) in [
|
||||
(style_ctx.text_style_names, style_ctx.active_text_style),
|
||||
(style_ctx.dim_style_names, style_ctx.active_dim_style),
|
||||
(
|
||||
style_ctx.mleader_style_names,
|
||||
style_ctx.active_mleader_style,
|
||||
),
|
||||
(style_ctx.table_style_names, style_ctx.active_table_style),
|
||||
] {
|
||||
names.hash(&mut hasher);
|
||||
active.hash(&mut hasher);
|
||||
}
|
||||
format!("{visibility:?}").hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
// ── Ribbon state ───────────────────────────────────────────────────────────
|
||||
|
||||
pub struct Ribbon {
|
||||
@@ -495,6 +634,8 @@ impl Ribbon {
|
||||
ts: widgets::ToggleState,
|
||||
style_ctx: &StyleContext<'_>,
|
||||
lead: &dyn Fn(bool) -> Vec<Element<'a, Message>>,
|
||||
mode: CollapseMode,
|
||||
flyout_open: bool,
|
||||
) -> Panel<'a> {
|
||||
let group = |compact: bool| {
|
||||
render_group(
|
||||
@@ -530,9 +671,21 @@ impl Ribbon {
|
||||
tight,
|
||||
)
|
||||
};
|
||||
let empty = || text("").into();
|
||||
let elements = match mode {
|
||||
CollapseMode::Auto => [group(false), group(true), button(false), button(true)],
|
||||
CollapseMode::Full => [group(false), empty(), empty(), empty()],
|
||||
CollapseMode::Compact => [empty(), group(true), empty(), empty()],
|
||||
CollapseMode::Collapsed => [
|
||||
if flyout_open { group(false) } else { empty() },
|
||||
empty(),
|
||||
button(false),
|
||||
empty(),
|
||||
],
|
||||
};
|
||||
Panel {
|
||||
id: g.title.to_string(),
|
||||
elements: [group(false), group(true), button(false), button(true)],
|
||||
elements,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -773,12 +926,14 @@ impl Ribbon {
|
||||
.padding([2, 8]);
|
||||
let mode_dd = PosReport::new(COLLAPSE_MODE_ID, mode_btn);
|
||||
|
||||
let mut tab_row = row![container(
|
||||
WrapBar::new(lead.into(), tabs.into())
|
||||
.spacing(6.0)
|
||||
.report_height(self.tab_bar_h.clone()),
|
||||
)
|
||||
.width(Length::Fill)]
|
||||
let mut tab_row = row![
|
||||
container(
|
||||
WrapBar::new(lead.into(), tabs.into())
|
||||
.spacing(6.0)
|
||||
.report_height(self.tab_bar_h.clone()),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
]
|
||||
.spacing(6.0)
|
||||
.align_y(iced::Center);
|
||||
if !self.collapse_tight.load(Ordering::Relaxed) {
|
||||
@@ -809,7 +964,7 @@ impl Ribbon {
|
||||
self.active
|
||||
};
|
||||
let tool_area: Element<'_, Message> = if self.modules.get(effective_active).is_some() {
|
||||
let groups = self.groups_for(effective_active);
|
||||
let groups: Vec<_> = self.groups_for(effective_active).collect();
|
||||
let style_ctx = StyleContext {
|
||||
text_style_names: &self.text_style_names,
|
||||
active_text_style: &self.active_text_style,
|
||||
@@ -826,18 +981,34 @@ impl Ribbon {
|
||||
// shrink to compact icon columns, then it collapses to a ▾ flyout
|
||||
// button. See `CollapsePanels`.
|
||||
let ts = self.toggle_state(show_block_palette);
|
||||
let measure_key = panel_measure_key(
|
||||
&groups,
|
||||
&self.last_cmd,
|
||||
&self.last_panel_tool,
|
||||
&style_ctx,
|
||||
visibility.as_ref(),
|
||||
);
|
||||
let panels: Vec<Panel<'_>> = groups
|
||||
.into_iter()
|
||||
.map(|g| {
|
||||
if self.modules[effective_active].id() == "block_editor"
|
||||
&& g.title == "Visibility"
|
||||
{
|
||||
visibility_panel::panel(visibility.clone())
|
||||
} else {
|
||||
self.panel(g, ts, &style_ctx, &|_| Vec::new())
|
||||
self.panel(
|
||||
g,
|
||||
ts,
|
||||
&style_ctx,
|
||||
&|_| Vec::new(),
|
||||
self.collapse_mode,
|
||||
self.collapsed_open.as_deref() == Some(g.title),
|
||||
)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H)
|
||||
.measure_key(measure_key)
|
||||
.report_height(self.tool_bar_h.clone())
|
||||
.report_tight(self.collapse_tight.clone())
|
||||
.mode(self.collapse_mode)
|
||||
@@ -1780,7 +1951,69 @@ impl Default for Ribbon {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::widgets::is_active_tool;
|
||||
use super::Ribbon;
|
||||
use super::{CollapseMode, Ribbon, StyleContext, panel_measure_key};
|
||||
use crate::modules::{IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
fn measurement_key(tool_id: &'static str, selected_command: &'static str) -> u64 {
|
||||
let groups = [RibbonGroup {
|
||||
title: "Test",
|
||||
tools: vec![RibbonItem::LargeTool(ToolDef {
|
||||
id: tool_id,
|
||||
label: "Test Tool",
|
||||
icon: IconKind::Glyph("T"),
|
||||
event: ModuleEvent::Command("TEST".to_string()),
|
||||
})],
|
||||
}];
|
||||
let style_ctx = StyleContext {
|
||||
text_style_names: &[],
|
||||
active_text_style: "",
|
||||
dim_style_names: &[],
|
||||
active_dim_style: "",
|
||||
mleader_style_names: &[],
|
||||
active_mleader_style: "",
|
||||
table_style_names: &[],
|
||||
active_table_style: "",
|
||||
};
|
||||
let groups: Vec<_> = groups.iter().collect();
|
||||
let mut last_cmd = FxHashMap::default();
|
||||
last_cmd.insert("TEST_DROPDOWN", selected_command);
|
||||
panel_measure_key(&groups, &last_cmd, &FxHashMap::default(), &style_ctx, None)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panel_measure_fingerprint_is_stable_and_tracks_panel_structure() {
|
||||
assert_eq!(
|
||||
measurement_key("TEST", "TEST"),
|
||||
measurement_key("TEST", "TEST")
|
||||
);
|
||||
assert_ne!(
|
||||
measurement_key("TEST", "TEST"),
|
||||
measurement_key("OTHER", "TEST")
|
||||
);
|
||||
assert_ne!(
|
||||
measurement_key("TEST", "TEST"),
|
||||
measurement_key("TEST", "OTHER")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forced_density_views_build_and_keep_collapsed_flyout_content() {
|
||||
let mut ribbon = Ribbon::default();
|
||||
let first_panel = ribbon.groups_for(ribbon.active).next().unwrap().title;
|
||||
|
||||
for mode in [
|
||||
CollapseMode::Full,
|
||||
CollapseMode::Compact,
|
||||
CollapseMode::Collapsed,
|
||||
] {
|
||||
ribbon.set_collapse_mode(mode);
|
||||
ribbon.collapsed_open =
|
||||
(mode == CollapseMode::Collapsed).then(|| first_panel.to_string());
|
||||
let view = ribbon.view(false, false, 0, 0, false, None);
|
||||
std::hint::black_box(view);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_test_close_panel_extends_draw_only_while_testing() {
|
||||
@@ -1790,10 +2023,12 @@ mod tests {
|
||||
.iter()
|
||||
.position(|module| module.id() == "draw")
|
||||
.unwrap();
|
||||
assert!(!ribbon
|
||||
.modules
|
||||
.iter()
|
||||
.any(|module| module.id() == "block_test"));
|
||||
assert!(
|
||||
!ribbon
|
||||
.modules
|
||||
.iter()
|
||||
.any(|module| module.id() == "block_test")
|
||||
);
|
||||
let ordinary: Vec<_> = ribbon.groups_for(draw).map(|group| group.title).collect();
|
||||
assert!(!ordinary.contains(&"Close"));
|
||||
ribbon.set_block_editing(true);
|
||||
|
||||
Reference in new issue
Block a user