Merge remote-tracking branch 'origin/main' into HEAD

This commit is contained in:
Hakan Seven committed 2026-08-29 15:58:18 +03:00
commit 1c52a2afcf
121 files changed
+946 -538

No files matched your search

+15 -5
View File
@@ -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() {
+4 -1
View File
@@ -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
View File
@@ -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#"
+38 -3
View File
@@ -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);
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
}
}
+2 -5
View File
@@ -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))
+21 -2
View File
@@ -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"));