forked from 1b-agent/opencadstudio-upstream
Merge remote-tracking branch 'origin/main' into HEAD
This commit is contained in:
commit
1c52a2afcf
121 files changed
+946
-538
No files matched your search
+15
-5
@@ -1435,13 +1435,23 @@ impl OpenCADStudio {
|
||||
// Selection-cycling list box: pick among overlapping objects.
|
||||
if let Some((pt, cands)) = &self.cycle_candidates {
|
||||
if !tab.is_start {
|
||||
let items: Vec<(acadrust::Handle, String)> = cands
|
||||
let items: Vec<crate::ui::popup::cycle_popup::CycleCandidate> = cands
|
||||
.iter()
|
||||
.filter_map(|&h| {
|
||||
tab.scene
|
||||
.document
|
||||
.get_entity(h)
|
||||
.map(|e| (h, crate::entities::traits::entity_type_name(e).to_string()))
|
||||
tab.scene.document.get_entity(h).map(|e| {
|
||||
let style = crate::scene::view::render::render_style_for_viewport(
|
||||
&tab.scene.document,
|
||||
e,
|
||||
None,
|
||||
);
|
||||
crate::ui::popup::cycle_popup::CycleCandidate {
|
||||
handle: h,
|
||||
type_name: crate::entities::traits::entity_type_name(e)
|
||||
.to_string(),
|
||||
layer: e.common().layer.clone(),
|
||||
color: style.0,
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
if !items.is_empty() {
|
||||
|
||||
@@ -45,7 +45,10 @@ pub struct PrintOptions {
|
||||
pub printer: Option<String>,
|
||||
/// Number of copies (treated as at least 1).
|
||||
pub copies: u32,
|
||||
/// Print quality label selected in the plot dialog.
|
||||
/// Print quality label selected in the plot dialog. Read only on the CUPS
|
||||
/// path (`lp -o print-quality=…`); on Windows the driver's own quality
|
||||
/// setting wins, so the field is legitimately unread there.
|
||||
#[cfg_attr(target_os = "windows", allow(dead_code))]
|
||||
pub quality: Option<String>,
|
||||
/// Controls applied while building the intermediate PDF.
|
||||
pub render: crate::io::pdf_export::PdfPlotOptions,
|
||||
|
||||
+160
-1
@@ -36,6 +36,12 @@ pub struct ReleaseInfo {
|
||||
pub acadrust_declared: bool,
|
||||
/// Whether the release matches this host.
|
||||
pub acadrust_compatible: bool,
|
||||
/// Full `rustc --version` output used by the release.
|
||||
pub rustc_version: Option<String>,
|
||||
/// Whether `[opencad]` declares `rustc_version`.
|
||||
pub rustc_declared: bool,
|
||||
/// Whether the release's rustc matches this host.
|
||||
pub rustc_compatible: bool,
|
||||
}
|
||||
|
||||
/// An add-on package found on disk (not necessarily loaded or compatible).
|
||||
@@ -54,6 +60,10 @@ pub struct ExternalPlugin {
|
||||
pub acadrust_source: Option<String>,
|
||||
/// Whether `[opencad]` declares `acadrust_source`.
|
||||
pub acadrust_declared: bool,
|
||||
/// Full `rustc --version` output used by the plugin.
|
||||
pub rustc_version: Option<String>,
|
||||
/// Whether `[opencad]` declares `rustc_version`.
|
||||
pub rustc_declared: bool,
|
||||
pub ribbon_order: i32,
|
||||
pub command_prefixes: Vec<String>,
|
||||
/// The package directory under the plugins folder.
|
||||
@@ -85,10 +95,27 @@ impl ExternalPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the package's rustc matches the host.
|
||||
pub fn rustc_compatible(&self) -> bool {
|
||||
if !ocs_plugin_api::version_info::uses_acadrust_gate(self.api_version) {
|
||||
return true;
|
||||
}
|
||||
match self.rustc_version.as_deref() {
|
||||
None | Some("") => false,
|
||||
Some(version) => ocs_plugin_api::version_info::rustc_versions_compatible(
|
||||
version,
|
||||
ocs_plugin_api::version_info::host_rustc_version(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the package can be loaded.
|
||||
#[allow(dead_code)] // plugin-host surface (issue #100); not yet wired
|
||||
pub fn loadable(&self) -> bool {
|
||||
self.api_compatible() && self.acadrust_compatible() && self.lib_present
|
||||
self.api_compatible()
|
||||
&& self.acadrust_compatible()
|
||||
&& self.rustc_compatible()
|
||||
&& self.lib_present
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +231,8 @@ pub(crate) fn parse_plugin_toml(text: &str) -> Option<ExternalPlugin> {
|
||||
let mut api_version: u32 = 0;
|
||||
let mut acadrust_source: Option<String> = None;
|
||||
let mut acadrust_declared = false;
|
||||
let mut rustc_version: Option<String> = None;
|
||||
let mut rustc_declared = false;
|
||||
let mut ribbon_order: i32 = 0;
|
||||
let mut command_prefixes: Vec<String> = Vec::new();
|
||||
let mut section = None;
|
||||
@@ -236,6 +265,11 @@ pub(crate) fn parse_plugin_toml(text: &str) -> Option<ExternalPlugin> {
|
||||
let v = unquote(value);
|
||||
acadrust_source = if v.is_empty() { None } else { Some(v) };
|
||||
}
|
||||
"rustc_version" if section == Some("opencad") => {
|
||||
rustc_declared = true;
|
||||
let v = unquote(value);
|
||||
rustc_version = if v.is_empty() { None } else { Some(v) };
|
||||
}
|
||||
"ribbon_order" => ribbon_order = value.parse().unwrap_or(0),
|
||||
"command_prefixes" => command_prefixes = parse_string_array(value),
|
||||
_ => {}
|
||||
@@ -251,6 +285,8 @@ pub(crate) fn parse_plugin_toml(text: &str) -> Option<ExternalPlugin> {
|
||||
api_version,
|
||||
acadrust_source,
|
||||
acadrust_declared,
|
||||
rustc_version,
|
||||
rustc_declared,
|
||||
ribbon_order,
|
||||
command_prefixes,
|
||||
dir: PathBuf::new(),
|
||||
@@ -372,6 +408,17 @@ mod loader {
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if !d.rustc_compatible() {
|
||||
let host_rustc = ocs_plugin_api::version_info::host_rustc_version();
|
||||
let plugin_rustc = d.rustc_version.as_deref().unwrap_or("unknown");
|
||||
out.push((
|
||||
d.id.clone(),
|
||||
Err(format!(
|
||||
"Plugin built with {plugin_rustc}, host requires {host_rustc} - rebuild required"
|
||||
)),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
let Some(path) = lib_file(&d.dir) else {
|
||||
out.push((
|
||||
d.id.clone(),
|
||||
@@ -483,6 +530,118 @@ xdata_apps = ["MYPLUGIN_RECORD"]
|
||||
assert!(!p.loadable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undeclared_rustc_is_incompatible() {
|
||||
let toml = r#"
|
||||
[plugin]
|
||||
id = "opencad.test"
|
||||
name = "Test"
|
||||
version = "0.1.0"
|
||||
api_version = 4
|
||||
"#;
|
||||
let p = parse_plugin_toml(toml).expect("parsed");
|
||||
assert!(!p.rustc_declared);
|
||||
assert!(p.rustc_version.is_none());
|
||||
assert!(!p.rustc_compatible());
|
||||
assert!(!p.loadable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declared_empty_rustc_version_is_incompatible() {
|
||||
let toml = r#"
|
||||
[plugin]
|
||||
id = "opencad.test"
|
||||
name = "Test"
|
||||
version = "0.1.0"
|
||||
api_version = 4
|
||||
|
||||
[opencad]
|
||||
rustc_version = ""
|
||||
"#;
|
||||
let p = parse_plugin_toml(toml).expect("parsed");
|
||||
assert!(p.rustc_declared);
|
||||
assert!(p.rustc_version.is_none());
|
||||
assert!(!p.rustc_compatible(), "declared but empty rustc is incompatible");
|
||||
assert!(!p.loadable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rustc_version_outside_opencad_is_ignored() {
|
||||
let toml = r#"
|
||||
[plugin]
|
||||
id = "opencad.test"
|
||||
api_version = 4
|
||||
rustc_version = "rustc 1.98.0"
|
||||
"#;
|
||||
let p = parse_plugin_toml(toml).expect("parsed");
|
||||
assert!(!p.rustc_declared);
|
||||
assert!(p.rustc_version.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rustc_mismatch_detected() {
|
||||
let _host = ocs_plugin_api::version_info::host_rustc_version();
|
||||
let other = "rustc 0.0.0-fake (not matching anything)";
|
||||
let toml = format!(
|
||||
r#"
|
||||
[plugin]
|
||||
id = "opencad.test"
|
||||
name = "Test"
|
||||
version = "0.1.0"
|
||||
api_version = 4
|
||||
|
||||
[opencad]
|
||||
rustc_version = "{other}"
|
||||
"#
|
||||
);
|
||||
let p = parse_plugin_toml(&toml).expect("parsed");
|
||||
assert!(p.rustc_declared);
|
||||
assert!(!p.rustc_compatible(), "mismatched rustc should be incompatible");
|
||||
assert!(!p.loadable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rustc_match_detected() {
|
||||
let host = ocs_plugin_api::version_info::host_rustc_version();
|
||||
let toml = format!(
|
||||
r#"
|
||||
[plugin]
|
||||
id = "opencad.test"
|
||||
name = "Test"
|
||||
version = "0.1.0"
|
||||
api_version = 4
|
||||
|
||||
[opencad]
|
||||
rustc_version = "{host}"
|
||||
"#
|
||||
);
|
||||
let p = parse_plugin_toml(&toml).expect("parsed");
|
||||
assert!(p.rustc_declared);
|
||||
assert!(p.rustc_compatible(), "matching rustc should be compatible");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rustc_gate_only_applies_to_api_v4_and_newer() {
|
||||
let toml = r#"
|
||||
[plugin]
|
||||
id = "opencad.test"
|
||||
name = "Test"
|
||||
version = "0.1.0"
|
||||
api_version = 2
|
||||
|
||||
[opencad]
|
||||
rustc_version = "rustc 0.0.0-fake"
|
||||
"#;
|
||||
let mut p = parse_plugin_toml(toml).expect("parsed");
|
||||
p.lib_present = true;
|
||||
assert!(p.rustc_declared);
|
||||
assert!(
|
||||
p.rustc_compatible(),
|
||||
"API v2 plugin should bypass rustc gate"
|
||||
);
|
||||
assert!(p.loadable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undeclared_acadrust_falls_back_to_api_gate() {
|
||||
let toml = r#"
|
||||
|
||||
@@ -236,12 +236,30 @@ pub fn fetch_release_info(repo: &str) -> Result<Vec<ReleaseInfo>, String> {
|
||||
),
|
||||
}
|
||||
};
|
||||
let rustc_version = manifest.rustc_version.clone();
|
||||
let rustc_declared = manifest.rustc_declared;
|
||||
let rustc_compatible = if !ocs_plugin_api::version_info::uses_acadrust_gate(
|
||||
manifest.api_version,
|
||||
) {
|
||||
true
|
||||
} else {
|
||||
match rustc_version.as_deref() {
|
||||
None | Some("") => false,
|
||||
Some(version) => ocs_plugin_api::version_info::rustc_versions_compatible(
|
||||
version,
|
||||
ocs_plugin_api::version_info::host_rustc_version(),
|
||||
),
|
||||
}
|
||||
};
|
||||
Ok::<_, String>(ReleaseInfo {
|
||||
tag: release.tag,
|
||||
api_version: manifest.api_version,
|
||||
acadrust_source,
|
||||
acadrust_declared,
|
||||
acadrust_compatible,
|
||||
rustc_version,
|
||||
rustc_declared,
|
||||
rustc_compatible,
|
||||
})
|
||||
})();
|
||||
match result {
|
||||
@@ -251,8 +269,8 @@ pub fn fetch_release_info(repo: &str) -> Result<Vec<ReleaseInfo>, String> {
|
||||
}
|
||||
|
||||
// Once a repo declares a fingerprint, require it on later API versions.
|
||||
let any_declared = info.iter().any(|r| r.acadrust_declared);
|
||||
if any_declared {
|
||||
let any_acadrust_declared = info.iter().any(|r| r.acadrust_declared);
|
||||
if any_acadrust_declared {
|
||||
for r in &mut info {
|
||||
if !r.acadrust_declared
|
||||
&& ocs_plugin_api::version_info::uses_acadrust_gate(r.api_version)
|
||||
@@ -261,7 +279,6 @@ pub fn fetch_release_info(repo: &str) -> Result<Vec<ReleaseInfo>, String> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if info.is_empty() {
|
||||
Err(last_error.unwrap_or_else(|| "no installable releases found".to_string()))
|
||||
} else {
|
||||
@@ -361,6 +378,24 @@ pub fn install(release: &Release, repository: &str) -> Result<String, String> {
|
||||
}
|
||||
}
|
||||
|
||||
if ocs_plugin_api::version_info::uses_acadrust_gate(manifest.api_version) {
|
||||
let Some(version) = manifest.rustc_version.as_deref() else {
|
||||
return Err("Release has no rustc version; cannot verify ABI compatibility".to_string());
|
||||
};
|
||||
if version.is_empty() {
|
||||
return Err("Release has no rustc version; cannot verify ABI compatibility".to_string());
|
||||
}
|
||||
if !ocs_plugin_api::version_info::rustc_versions_compatible(
|
||||
version,
|
||||
ocs_plugin_api::version_info::host_rustc_version(),
|
||||
) {
|
||||
let host_rustc = ocs_plugin_api::version_info::host_rustc_version();
|
||||
return Err(format!(
|
||||
"Plugin built with {version}, host requires {host_rustc} - rebuild required"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let dir = external::plugins_dir()
|
||||
.ok_or("cannot locate the plugins folder")?
|
||||
.join(&manifest.id);
|
||||
|
||||
@@ -2802,7 +2802,7 @@ fn viewport_override(
|
||||
.find_map(|(handle, value)| (handle == viewport).then_some(value))
|
||||
}
|
||||
|
||||
pub(in crate::scene) fn render_style_for_viewport(
|
||||
pub(crate) fn render_style_for_viewport(
|
||||
document: &CadDocument,
|
||||
e: &EntityType,
|
||||
viewport: Option<Handle>,
|
||||
|
||||
+12
-3
@@ -346,13 +346,19 @@ fn semantic_color<'a>(
|
||||
token: &[u8],
|
||||
colors: &'a SemanticColors,
|
||||
) -> Option<&'a [u8; 7]> {
|
||||
if is_one_of(token, &["#e0e0e0", "#eeeeee", "#ffffff", "#e1e1e1"]) {
|
||||
if is_one_of(
|
||||
token,
|
||||
&["#b4b6b9", "#e0e0e0", "#eeeeee", "#ffffff", "#e1e1e1"],
|
||||
) {
|
||||
Some(&colors.text)
|
||||
} else if is_one_of(token, &["#cccccc", "#bdbdbd", "#aaaaaa"]) {
|
||||
Some(&colors.secondary_strong)
|
||||
} else if is_one_of(
|
||||
token,
|
||||
&["#888888", "#888", "#9e9e9e", "#90a4ae", "#78909c", "#7a7a7a"],
|
||||
&[
|
||||
"#888888", "#888", "#9e9e9e", "#90a4ae", "#78909c", "#7a7a7a",
|
||||
"#777777",
|
||||
],
|
||||
) {
|
||||
Some(&colors.secondary)
|
||||
} else if is_one_of(
|
||||
@@ -365,7 +371,10 @@ fn semantic_color<'a>(
|
||||
Some(&colors.secondary_weak)
|
||||
} else if is_one_of(token, &["#1a1a1a"]) {
|
||||
Some(&colors.background)
|
||||
} else if is_one_of(token, &["#4cc9f0", "#4bc8f0", "#4a9eff", "#0099e5"]) {
|
||||
} else if is_one_of(
|
||||
token,
|
||||
&["#6db7ed", "#4cc9f0", "#4bc8f0", "#4a9eff", "#0099e5"],
|
||||
) {
|
||||
Some(&colors.primary)
|
||||
} else if is_one_of(token, &["#1565c0"]) {
|
||||
Some(&colors.primary_strong)
|
||||
|
||||
+77
-10
@@ -1,26 +1,43 @@
|
||||
//! Selection-cycling list box — shown at the cursor when a click lands on
|
||||
//! two or more overlapping objects. Each row names a candidate; clicking it
|
||||
//! adds that object to the current selection. Clicking outside dismisses it.
|
||||
//! two or more overlapping objects. Each row shows the object's drawn color
|
||||
//! swatch, its type, and (right-aligned) its layer; clicking a row adds that
|
||||
//! object to the current selection. Clicking outside dismisses it.
|
||||
|
||||
use iced::widget::{button, column, container, mouse_area, opaque, text};
|
||||
use iced::{Element, Fill, Length};
|
||||
use iced::alignment::Horizontal;
|
||||
use iced::widget::{button, column, container, mouse_area, opaque, row, rule, space, text};
|
||||
use iced::{border::Radius, color, Element, Fill, Length, Theme};
|
||||
|
||||
use crate::app::Message;
|
||||
|
||||
/// Row content for one overlapping candidate.
|
||||
pub struct CycleCandidate {
|
||||
pub handle: acadrust::Handle,
|
||||
pub type_name: String,
|
||||
pub layer: String,
|
||||
/// The color the object is drawn with (layer-inherited when ByLayer).
|
||||
pub color: [f32; 4],
|
||||
}
|
||||
|
||||
/// Full-canvas overlay: the list box anchored at `anchor` (canvas
|
||||
/// coordinates) plus a transparent click-catcher that cancels.
|
||||
pub fn cycle_popup_overlay(
|
||||
anchor: iced::Point,
|
||||
items: Vec<(acadrust::Handle, String)>,
|
||||
items: Vec<CycleCandidate>,
|
||||
) -> Element<'static, Message> {
|
||||
// The type column and the layer column get one shared width each (the
|
||||
// widest entry of their kind, clamped) so the separators line up.
|
||||
let type_w = clamp_col_width(items.iter().map(|c| c.type_name.chars().count()), 11.0, 40.0, 96.0);
|
||||
let layer_w =
|
||||
clamp_col_width(items.iter().map(|c| c.layer.chars().count()), 10.0, 28.0, 84.0);
|
||||
|
||||
let rows: Vec<Element<'static, Message>> = items
|
||||
.into_iter()
|
||||
.map(|(handle, label)| item_row(handle, label))
|
||||
.map(|c| item_row(c.handle, c.type_name, c.layer, c.color, type_w, layer_w))
|
||||
.collect();
|
||||
|
||||
let panel = container(column(rows))
|
||||
.style(container::bordered_box)
|
||||
.width(Length::Fixed(150.0));
|
||||
.width(Length::Fixed(type_w + layer_w + 42.0));
|
||||
|
||||
let positioned = iced::widget::pin(opaque(panel))
|
||||
.position(iced::Point::new(anchor.x.max(0.0), anchor.y.max(0.0)));
|
||||
@@ -28,13 +45,63 @@ pub fn cycle_popup_overlay(
|
||||
mouse_area(positioned).on_press(Message::CycleCancel).into()
|
||||
}
|
||||
|
||||
fn item_row(handle: acadrust::Handle, label: String) -> Element<'static, Message> {
|
||||
let content = text(label).size(11).align_y(iced::Center);
|
||||
/// Column width for the widest label, using a per-glyph estimate with a
|
||||
/// minimum so short labels don't collapse and a maximum so long ones clip.
|
||||
fn clamp_col_width(labels: impl Iterator<Item = usize>, glyph_w: f32, min: f32, max: f32) -> f32 {
|
||||
let widest = labels.max().unwrap_or(0) as f32 * glyph_w;
|
||||
widest.clamp(min, max)
|
||||
}
|
||||
|
||||
fn item_row(
|
||||
handle: acadrust::Handle,
|
||||
type_name: String,
|
||||
layer: String,
|
||||
color: [f32; 4],
|
||||
type_w: f32,
|
||||
layer_w: f32,
|
||||
) -> Element<'static, Message> {
|
||||
let swatch = container(
|
||||
space::Space::new()
|
||||
.width(Length::Fixed(9.0))
|
||||
.height(Length::Fixed(9.0)),
|
||||
)
|
||||
.style(move |_| container::Style {
|
||||
background: Some(iced::Background::Color(iced::Color::from_rgba(
|
||||
color[0], color[1], color[2], color[3],
|
||||
))),
|
||||
border: iced::Border {
|
||||
color: color!(0x6b6b6b),
|
||||
width: 1.0,
|
||||
radius: Radius::from(2.0),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let content = row![
|
||||
swatch,
|
||||
container(text(type_name).size(11))
|
||||
.width(Length::Fixed(type_w))
|
||||
.align_y(iced::alignment::Vertical::Center),
|
||||
container(rule::vertical(1)).height(Length::Fixed(12.0)),
|
||||
container(
|
||||
text(layer)
|
||||
.size(10)
|
||||
.align_x(Horizontal::Right)
|
||||
.style(|theme: &Theme| iced::widget::text::Style {
|
||||
color: Some(theme.palette().background.base.text.scale_alpha(0.72)),
|
||||
})
|
||||
)
|
||||
.width(Length::Fixed(layer_w))
|
||||
.align_y(iced::alignment::Vertical::Center),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center);
|
||||
|
||||
let btn = button(content)
|
||||
.on_press(Message::CycleSelect(handle))
|
||||
.style(button::subtle)
|
||||
.width(Fill)
|
||||
.padding([4, 10]);
|
||||
.padding([3, 8]);
|
||||
// Highlight the underlying object while the cursor is over this row.
|
||||
mouse_area(btn)
|
||||
.on_enter(Message::CycleHover(Some(handle)))
|
||||
|
||||
+51
-42
@@ -1303,6 +1303,20 @@ fn render_group<'a>(
|
||||
let mut items_row: Vec<Element<Message>> = Vec::new();
|
||||
let mut small_buf: Vec<Element<Message>> = Vec::new();
|
||||
|
||||
let ctx = widgets::RenderCtx {
|
||||
active_tool,
|
||||
open_dd,
|
||||
last_cmd,
|
||||
state,
|
||||
layer_infos,
|
||||
active_layer,
|
||||
active_color,
|
||||
active_linetype,
|
||||
active_lineweight,
|
||||
style_ctx,
|
||||
compact,
|
||||
};
|
||||
|
||||
for item in &group.tools {
|
||||
let is_large = match item {
|
||||
RibbonItem::LargeTool(_) | RibbonItem::LargeDropdown { .. } => !compact,
|
||||
@@ -1314,20 +1328,7 @@ fn render_group<'a>(
|
||||
|
||||
if is_large {
|
||||
flush_small_col(&mut small_buf, &mut items_row);
|
||||
items_row.push(render_large(
|
||||
item,
|
||||
active_tool,
|
||||
open_dd,
|
||||
last_cmd,
|
||||
state,
|
||||
layer_infos,
|
||||
active_layer,
|
||||
active_color,
|
||||
active_linetype,
|
||||
active_lineweight,
|
||||
style_ctx,
|
||||
compact,
|
||||
));
|
||||
items_row.push(render_large(item, &ctx));
|
||||
} else {
|
||||
small_buf.push(render_small(
|
||||
item,
|
||||
@@ -1467,34 +1468,42 @@ fn collapse_button<'a>(
|
||||
// For a Properties panel the representative is its Match button.
|
||||
let rep = representative(group, last_used);
|
||||
let face: Element<'_, Message> = match rep {
|
||||
Some(RibbonItem::PropertiesGroup { match_prop }) => render_large(
|
||||
&RibbonItem::LargeTool(match_prop.clone()),
|
||||
active_tool,
|
||||
open_dd,
|
||||
last_cmd,
|
||||
state,
|
||||
layer_infos,
|
||||
active_layer,
|
||||
active_color,
|
||||
active_linetype,
|
||||
active_lineweight,
|
||||
style_ctx,
|
||||
false,
|
||||
),
|
||||
Some(item) => render_large(
|
||||
item,
|
||||
active_tool,
|
||||
open_dd,
|
||||
last_cmd,
|
||||
state,
|
||||
layer_infos,
|
||||
active_layer,
|
||||
active_color,
|
||||
active_linetype,
|
||||
active_lineweight,
|
||||
style_ctx,
|
||||
false,
|
||||
),
|
||||
Some(RibbonItem::PropertiesGroup { match_prop }) => {
|
||||
render_large(
|
||||
&RibbonItem::LargeTool(match_prop.clone()),
|
||||
&widgets::RenderCtx {
|
||||
active_tool,
|
||||
open_dd,
|
||||
last_cmd,
|
||||
state,
|
||||
layer_infos,
|
||||
active_layer,
|
||||
active_color,
|
||||
active_linetype,
|
||||
active_lineweight,
|
||||
style_ctx,
|
||||
compact: false,
|
||||
},
|
||||
)
|
||||
}
|
||||
Some(item) => {
|
||||
render_large(
|
||||
item,
|
||||
&widgets::RenderCtx {
|
||||
active_tool,
|
||||
open_dd,
|
||||
last_cmd,
|
||||
state,
|
||||
layer_infos,
|
||||
active_layer,
|
||||
active_color,
|
||||
active_linetype,
|
||||
active_lineweight,
|
||||
style_ctx,
|
||||
compact: false,
|
||||
},
|
||||
)
|
||||
}
|
||||
None => text("").into(),
|
||||
};
|
||||
|
||||
|
||||
+111
-134
@@ -70,6 +70,18 @@ pub(super) const TOOL_BAR_H: f32 = 3.0 * ROW_H + 18.0;
|
||||
/// shorter than a full 3-row panel — the ribbon height follows it down.
|
||||
pub(super) const COLLAPSED_FACE_H: f32 = LARGE_ICON + 20.0;
|
||||
|
||||
/// Gap between the combo dropdown and the tool rows beneath it.
|
||||
const COMBO_ROW_SPACING: f32 = 3.0;
|
||||
/// Shared padding of combo-style panels (Layers, Properties, style selectors).
|
||||
/// The top inset pins every primary dropdown to the same baseline regardless
|
||||
/// of how many icon rows sit underneath it.
|
||||
const COMBO_PANEL_PAD: Padding = Padding {
|
||||
top: 6.0,
|
||||
bottom: 4.0,
|
||||
left: 4.0,
|
||||
right: 4.0,
|
||||
};
|
||||
|
||||
// ── Automatic large-button sizing ────────────────────────────────────────
|
||||
|
||||
thread_local! {
|
||||
@@ -604,21 +616,40 @@ pub(super) fn render_small<'a>(
|
||||
|
||||
// ── Large item renderer ────────────────────────────────────────────────────
|
||||
|
||||
/// Shared render context threaded through the large-item render path. Bundling
|
||||
/// the per-view state keeps new fields from rippling through every render
|
||||
/// signature and call site (same idea as `ToggleState`).
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) struct RenderCtx<'a> {
|
||||
pub active_tool: &'a Option<String>,
|
||||
pub open_dd: &'a Option<String>,
|
||||
pub last_cmd: &'a HashMap<&'static str, &'static str>,
|
||||
pub state: ToggleState,
|
||||
pub layer_infos: &'a [LayerInfo],
|
||||
pub active_layer: &'a str,
|
||||
pub active_color: AcadColor,
|
||||
pub active_linetype: &'a str,
|
||||
pub active_lineweight: LineWeight,
|
||||
pub style_ctx: &'a StyleContext,
|
||||
/// When compact, the Properties panel's Match button shrinks to a small icon.
|
||||
pub compact: bool,
|
||||
}
|
||||
|
||||
/// A large dropdown button: the current icon on top, its ▾ directly beneath the
|
||||
/// icon, then the label at the bottom. Shared by `LargeDropdown` / `Dropdown` in
|
||||
/// the full ribbon and by a collapsed panel whose representative is a dropdown.
|
||||
/// `explicit_label` overrides the derived (current-item) label when given.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn render_large_dropdown<'a>(
|
||||
id: &'static str,
|
||||
icon: IconKind,
|
||||
explicit_label: Option<&'a str>,
|
||||
items: &[(&'static str, &'static str, IconKind)],
|
||||
default: &'static str,
|
||||
active_tool: &Option<String>,
|
||||
open_dd: &Option<String>,
|
||||
last_cmd: &HashMap<&'static str, &'static str>,
|
||||
ctx: &RenderCtx<'_>,
|
||||
) -> Element<'a, Message> {
|
||||
let active_tool = ctx.active_tool;
|
||||
let open_dd = ctx.open_dd;
|
||||
let last_cmd = ctx.last_cmd;
|
||||
let active = active_tool.as_deref() == Some(id)
|
||||
|| items
|
||||
.iter()
|
||||
@@ -708,22 +739,65 @@ pub(super) fn render_large_dropdown<'a>(
|
||||
.into()
|
||||
}
|
||||
|
||||
/// A row of small tool buttons beneath a combo dropdown. Shared by the
|
||||
/// Layers, Properties and style-combo panels so their icon rows render
|
||||
/// identically.
|
||||
fn tool_row<'a>(tools: &[ToolDef], active_tool: &Option<String>) -> Element<'a, Message> {
|
||||
let btns: Vec<Element<Message>> = tools
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let is_active = active_tool.as_deref() == Some(t.id);
|
||||
let tip = t!(t.label);
|
||||
let event = t.event.clone();
|
||||
let icon_el: Element<Message> = make_icon(t.icon, 16.0);
|
||||
let msg = module_event_to_message(event);
|
||||
tooltip(
|
||||
button(icon_el)
|
||||
.on_press(msg)
|
||||
.style(move |theme: &Theme, status| {
|
||||
tool_btn_style(theme, is_active, status)
|
||||
})
|
||||
.padding([2, 5]),
|
||||
make_tip(tip.to_string()),
|
||||
TipPos::Right,
|
||||
)
|
||||
.gap(4.0)
|
||||
.delay(Duration::from_millis(400))
|
||||
.style(tip_style)
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
row(btns).spacing(2).align_y(iced::Center).into()
|
||||
}
|
||||
|
||||
/// Fixed-height wrapper for a combo-style panel: dropdown on top, icon rows
|
||||
/// below, all pinned to the panel's top edge so every panel's primary
|
||||
/// dropdown shares one baseline.
|
||||
fn combo_panel_col(width: f32, items: Vec<Element<'_, Message>>) -> Element<'_, Message> {
|
||||
container(column(items).spacing(COMBO_ROW_SPACING).align_x(iced::Left))
|
||||
.width(Length::Fixed(width))
|
||||
.height(Fill)
|
||||
.align_y(iced::Top)
|
||||
.padding(COMBO_PANEL_PAD)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Render a full-height large button (LargeTool, LargeDropdown, LayerCombo, StyleCombo).
|
||||
pub(super) fn render_large<'a>(
|
||||
item: &RibbonItem,
|
||||
active_tool: &Option<String>,
|
||||
open_dd: &Option<String>,
|
||||
last_cmd: &HashMap<&'static str, &'static str>,
|
||||
state: ToggleState,
|
||||
layer_infos: &'a [LayerInfo],
|
||||
active_layer: &'a str,
|
||||
active_color: AcadColor,
|
||||
active_linetype: &'a str,
|
||||
active_lineweight: LineWeight,
|
||||
style_ctx: &StyleContext,
|
||||
// When compact, the Properties panel's Match button shrinks to a small icon.
|
||||
compact: bool,
|
||||
ctx: &RenderCtx<'_>,
|
||||
) -> Element<'a, Message> {
|
||||
let active_tool = ctx.active_tool;
|
||||
let open_dd = ctx.open_dd;
|
||||
let last_cmd = ctx.last_cmd;
|
||||
let state = ctx.state;
|
||||
let layer_infos = ctx.layer_infos;
|
||||
let active_layer = ctx.active_layer;
|
||||
let active_color = ctx.active_color;
|
||||
let active_linetype = ctx.active_linetype;
|
||||
let active_lineweight = ctx.active_lineweight;
|
||||
let style_ctx = ctx.style_ctx;
|
||||
let compact = ctx.compact;
|
||||
match item {
|
||||
// A plain Tool renders large too, so a collapsed panel can show its
|
||||
// representative tool as a big icon.
|
||||
@@ -779,16 +853,7 @@ pub(super) fn render_large<'a>(
|
||||
items,
|
||||
default,
|
||||
} => {
|
||||
render_large_dropdown(
|
||||
*id,
|
||||
*icon,
|
||||
Some(*label),
|
||||
items,
|
||||
*default,
|
||||
active_tool,
|
||||
open_dd,
|
||||
last_cmd,
|
||||
)
|
||||
render_large_dropdown(*id, *icon, Some(*label), items, *default, ctx)
|
||||
}
|
||||
|
||||
// A plain Dropdown renders large too (used by a collapsed panel whose
|
||||
@@ -799,9 +864,7 @@ pub(super) fn render_large<'a>(
|
||||
items,
|
||||
default,
|
||||
} => {
|
||||
render_large_dropdown(
|
||||
*id, *icon, None, items, *default, active_tool, open_dd, last_cmd,
|
||||
)
|
||||
render_large_dropdown(*id, *icon, None, items, *default, ctx)
|
||||
}
|
||||
|
||||
RibbonItem::LayerComboGroup { row2, row3 } => {
|
||||
@@ -868,56 +931,14 @@ pub(super) fn render_large<'a>(
|
||||
.padding([3, 8])
|
||||
.width(Fill);
|
||||
|
||||
let make_tool_row = |tools: &[ToolDef]| -> Element<Message> {
|
||||
let btns: Vec<Element<Message>> = tools
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let is_active = active_tool.as_deref() == Some(t.id);
|
||||
let tip = t!(t.label);
|
||||
let event = t.event.clone();
|
||||
let icon_el: Element<Message> = make_icon(t.icon, 16.0);
|
||||
let msg = module_event_to_message(event);
|
||||
tooltip(
|
||||
button(icon_el)
|
||||
.on_press(msg)
|
||||
.style(move |theme: &Theme, status| {
|
||||
tool_btn_style(theme, is_active, status)
|
||||
})
|
||||
.padding([2, 5]),
|
||||
make_tip(tip.to_string()),
|
||||
TipPos::Right,
|
||||
)
|
||||
.gap(4.0)
|
||||
.delay(Duration::from_millis(400))
|
||||
.style(tip_style)
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
row(btns).spacing(2).align_y(iced::Center).into()
|
||||
};
|
||||
|
||||
let tools_row2 = make_tool_row(row2);
|
||||
let tools_row3 = make_tool_row(row3);
|
||||
|
||||
container(
|
||||
column![
|
||||
PosReport::new(LAYER_COMBO_ID, combo_btn),
|
||||
tools_row2,
|
||||
tools_row3
|
||||
]
|
||||
.spacing(3)
|
||||
.align_x(iced::Left),
|
||||
combo_panel_col(
|
||||
combo_w,
|
||||
vec![
|
||||
container(PosReport::new(LAYER_COMBO_ID, combo_btn)).width(Fill).into(),
|
||||
tool_row(row2, active_tool),
|
||||
tool_row(row3, active_tool),
|
||||
],
|
||||
)
|
||||
.width(Length::Fixed(combo_w))
|
||||
.height(Fill)
|
||||
.align_y(iced::Center)
|
||||
.padding(Padding {
|
||||
top: 4.0,
|
||||
bottom: 4.0,
|
||||
left: 4.0,
|
||||
right: 4.0,
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
RibbonItem::PropertiesGroup { match_prop } => {
|
||||
@@ -934,17 +955,10 @@ pub(super) fn render_large<'a>(
|
||||
} else {
|
||||
render_large(
|
||||
&RibbonItem::LargeTool(match_prop.clone()),
|
||||
active_tool,
|
||||
open_dd,
|
||||
last_cmd,
|
||||
state,
|
||||
layer_infos,
|
||||
active_layer,
|
||||
active_color,
|
||||
active_linetype,
|
||||
active_lineweight,
|
||||
style_ctx,
|
||||
false,
|
||||
&RenderCtx {
|
||||
compact: false,
|
||||
..*ctx
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
@@ -1005,6 +1019,8 @@ pub(super) fn render_large<'a>(
|
||||
);
|
||||
let lw_row = prop_row(LwItem(active_lineweight).to_string(), PROP_LW_ID, None);
|
||||
|
||||
// Top-pinned like every combo panel, so the Color dropdown shares
|
||||
// the same baseline as the Layers / style selectors.
|
||||
let combos = container(
|
||||
column![
|
||||
PosReport::new(PROP_COLOR_ID, color_row),
|
||||
@@ -1015,12 +1031,12 @@ pub(super) fn render_large<'a>(
|
||||
.align_x(iced::Left),
|
||||
)
|
||||
.height(Fill)
|
||||
.align_y(iced::Center)
|
||||
.align_y(iced::Top)
|
||||
.padding(Padding {
|
||||
top: 4.0,
|
||||
bottom: 4.0,
|
||||
top: COMBO_PANEL_PAD.top,
|
||||
bottom: COMBO_PANEL_PAD.bottom,
|
||||
left: 0.0,
|
||||
right: 4.0,
|
||||
right: COMBO_PANEL_PAD.right,
|
||||
});
|
||||
|
||||
row![mp_el, combos]
|
||||
@@ -1068,54 +1084,15 @@ pub(super) fn render_large<'a>(
|
||||
let items_panel: Element<Message> =
|
||||
iced::widget::Space::new().width(0).height(0).into();
|
||||
|
||||
// ── tool rows below combo ──
|
||||
let make_tool_row = |tools: &[ToolDef]| -> Element<Message> {
|
||||
let btns: Vec<Element<Message>> = tools
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let is_active = active_tool.as_deref() == Some(t.id);
|
||||
let tip = t!(t.label);
|
||||
let event = t.event.clone();
|
||||
let icon_el: Element<Message> = make_icon(t.icon, 16.0);
|
||||
let msg = module_event_to_message(event);
|
||||
tooltip(
|
||||
button(icon_el)
|
||||
.on_press(msg)
|
||||
.style(move |theme: &Theme, status| {
|
||||
tool_btn_style(theme, is_active, status)
|
||||
})
|
||||
.padding([2, 5]),
|
||||
make_tip(tip.to_string()),
|
||||
TipPos::Right,
|
||||
)
|
||||
.gap(4.0)
|
||||
.delay(Duration::from_millis(400))
|
||||
.style(tip_style)
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
row(btns).spacing(2).align_y(iced::Center).into()
|
||||
};
|
||||
|
||||
let mut col_items: Vec<Element<Message>> =
|
||||
vec![container(row![PosReport::new(*combo_id, combo_btn), items_panel].spacing(0))
|
||||
.width(Fill)
|
||||
.into()];
|
||||
for row_tools in rows {
|
||||
col_items.push(make_tool_row(row_tools));
|
||||
col_items.push(tool_row(row_tools, active_tool));
|
||||
}
|
||||
|
||||
container(column(col_items).spacing(3).align_x(iced::Left))
|
||||
.width(Length::Fixed(STYLE_COMBO_W))
|
||||
.height(Fill)
|
||||
.align_y(iced::Center)
|
||||
.padding(Padding {
|
||||
top: 4.0,
|
||||
bottom: 4.0,
|
||||
left: 4.0,
|
||||
right: 4.0,
|
||||
})
|
||||
.into()
|
||||
combo_panel_col(STYLE_COMBO_W, col_items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::modules::IconKind;
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use crate::ui::dock::{DockMsg, PanelId};
|
||||
use iced::widget::canvas::{Frame, Path, Program, Stroke};
|
||||
use iced::widget::{button, canvas, column, container, mouse_area, row, scrollable, svg, text, text_input, tooltip};
|
||||
use iced::widget::{button, canvas, column, container, mouse_area, row, scrollable, text, text_input, tooltip};
|
||||
use iced::{Background, Border, Color, Element, Fill, Length, Theme};
|
||||
|
||||
const TOOL_H: f32 = 22.0;
|
||||
@@ -355,10 +355,7 @@ fn block_card<'a>(palette: &'a BlockPalette, block: &'a BlockEntry) -> Element<'
|
||||
fn icon_button<'a>(icon: IconKind, msg: BlockPaletteMsg) -> Element<'a, Message> {
|
||||
let icon_el: Element<'_, Message> = match icon {
|
||||
IconKind::Glyph(s) => text(s).size(15).color(Color::WHITE).into(),
|
||||
IconKind::Svg(bytes) => svg(svg::Handle::from_memory(bytes))
|
||||
.width(Length::Fixed(TOOL_H))
|
||||
.height(Length::Fixed(TOOL_H))
|
||||
.into(),
|
||||
IconKind::Svg(bytes) => crate::ui::icons::semantic(bytes, TOOL_H),
|
||||
};
|
||||
button(icon_el)
|
||||
.on_press(Message::BlockPalette(msg))
|
||||
|
||||
@@ -203,6 +203,7 @@ fn newest_update(installed: &str, releases: &[ReleaseInfo]) -> Option<String> {
|
||||
.filter(|release| {
|
||||
ocs_plugin_api::manifest::host_accepts_plugin_version(release.api_version)
|
||||
&& release.acadrust_compatible
|
||||
&& release.rustc_compatible
|
||||
})
|
||||
.filter_map(|release| {
|
||||
semver::Version::parse(trim_version_prefix(&release.tag))
|
||||
@@ -225,13 +226,15 @@ fn external_card<'a>(
|
||||
) -> Element<'a, Message> {
|
||||
let failed_old_api = load_error.is_some() && !p.api_compatible();
|
||||
let acadrust_mismatch = p.acadrust_declared && !p.acadrust_compatible();
|
||||
let rustc_mismatch = ocs_plugin_api::version_info::uses_acadrust_gate(p.api_version)
|
||||
&& !p.rustc_compatible();
|
||||
let (status, kind) = if loaded && disabled {
|
||||
(t!("Disabled"), StatusKind::Muted)
|
||||
} else if loaded {
|
||||
(t!("Loaded"), StatusKind::Success)
|
||||
} else if !p.api_compatible() || failed_old_api {
|
||||
(t!("API incompatible"), StatusKind::Danger)
|
||||
} else if acadrust_mismatch {
|
||||
} else if acadrust_mismatch || rustc_mismatch {
|
||||
(t!("Incompatible"), StatusKind::Danger)
|
||||
} else if load_error.is_some() {
|
||||
(t!("Load failed"), StatusKind::Danger)
|
||||
@@ -375,7 +378,8 @@ fn install_controls<'a>(
|
||||
let action = match selected_release {
|
||||
Some(release)
|
||||
if ocs_plugin_api::manifest::host_accepts_plugin_version(release.api_version)
|
||||
&& release.acadrust_compatible =>
|
||||
&& release.acadrust_compatible
|
||||
&& release.rustc_compatible =>
|
||||
{
|
||||
pill_button(
|
||||
t!("Install"),
|
||||
@@ -990,6 +994,9 @@ mod tests {
|
||||
acadrust_source: None,
|
||||
acadrust_declared: false,
|
||||
acadrust_compatible: true,
|
||||
rustc_version: None,
|
||||
rustc_declared: false,
|
||||
rustc_compatible: true,
|
||||
},
|
||||
ReleaseInfo {
|
||||
tag: "v2.0.0".to_string(),
|
||||
@@ -997,6 +1004,9 @@ mod tests {
|
||||
acadrust_source: None,
|
||||
acadrust_declared: false,
|
||||
acadrust_compatible: true,
|
||||
rustc_version: None,
|
||||
rustc_declared: false,
|
||||
rustc_compatible: true,
|
||||
},
|
||||
ReleaseInfo {
|
||||
tag: "v1.9.9".to_string(),
|
||||
@@ -1004,6 +1014,9 @@ mod tests {
|
||||
acadrust_source: None,
|
||||
acadrust_declared: false,
|
||||
acadrust_compatible: true,
|
||||
rustc_version: None,
|
||||
rustc_declared: false,
|
||||
rustc_compatible: true,
|
||||
},
|
||||
];
|
||||
assert_eq!(newest_update("1.3.0", &releases).as_deref(), Some("v2.0.0"));
|
||||
@@ -1021,6 +1034,9 @@ mod tests {
|
||||
),
|
||||
acadrust_declared: true,
|
||||
acadrust_compatible: true,
|
||||
rustc_version: None,
|
||||
rustc_declared: false,
|
||||
rustc_compatible: true,
|
||||
},
|
||||
ReleaseInfo {
|
||||
tag: "v2.0.0".to_string(),
|
||||
@@ -1030,6 +1046,9 @@ mod tests {
|
||||
),
|
||||
acadrust_declared: true,
|
||||
acadrust_compatible: false,
|
||||
rustc_version: None,
|
||||
rustc_declared: false,
|
||||
rustc_compatible: true,
|
||||
},
|
||||
];
|
||||
assert_eq!(newest_update("1.3.0", &releases).as_deref(), Some("v1.4.0"));
|
||||
|
||||
Reference in new issue
Block a user