欢迎光临
我们一直在努力

Rust+Slint 实现蜂巢菜单源码分享

在这里插入图片描述

Rust+Slint 实现蜂巢菜单源码分享

  • 一、效果展示
  • 二、源码分享
    • 1、工程结构
    • 2、main.rs
    • 3、build.rs
    • 4、main.slint
    • 5、Cargo.toml
  • 三、实现原理
    • 1、蜂巢网格布局
    • 2、圆形裁剪与可见性控制
    • 3、鱼眼凸起效果(Fisheye Bulge)
    • 4、鼠标交互与点击检测
      • 凸起跟随
      • 点击检测(Rust 回调)
    • 5、数据流与响应式更新
    • 6、小结

一、效果展示

在这里插入图片描述

在这里插入图片描述 在这里插入图片描述

在这里插入图片描述

二、源码分享

1、工程结构

在这里插入图片描述

2、main.rs

slint::include_modules!();

use slint::{Model, ModelRc};

fn main() {
let ui = MainWindow::new().expect("无法创建主窗口");

// ─── 读取 Slint 中的 icon_data(含位置信息)───
let icon_data_model = ui.get_icon_data();
let icon_count = icon_data_model.row_count();

// 缓存图标位置数据,用于点击检测
let positions: Vec<(f32, f32, f32, i32)> = (0..icon_count)
.map(|i| {
let d = icon_data_model.row_data(i).unwrap();
(d.base_x, d.base_y, d.sz, d.icon_index)
})
.collect();

println!("已加载 {} 个图标位置", positions.len());

// ─── 点击检测回调 ───
// 遍历 icon_data,找距离点击位置最近的图标(在 sz*0.7 范围内)
let positions_clone = positions.clone();
ui.on_find_icon_at(move |click_x, click_y| -> i32 {
let mut closest_idx: i32 = 1;
let mut closest_dist = f32::MAX;

for (base_x, base_y, sz, icon_index) in &positions_clone {
let dx = *base_x click_x as f32;
let dy = *base_y click_y as f32;
let dist = (dx * dx + dy * dy).sqrt();
let hit_radius = sz * 0.7;

if dist <= hit_radius && dist < closest_dist {
closest_dist = dist;
closest_idx = *icon_index;
}
}

closest_idx
});

ui.run().expect("运行 UI 失败");
}

3、build.rs

fn main(){
slint_build::compile("ui/main.slint")
.unwrap()
}

4、main.slint

export struct IconData {
base_x: float,
base_y: float,
sz: float,
icon_index: int,
name: string,
}

export component MainWindow inherits Window {
title: "蜂巢菜单";
preferredwidth: 700px;
preferredheight: 440px;
background: #0a0a0a;

// ─── 凸起效果中心(跟随鼠标)───
property <float> bulge_cx: 350;
property <float> bulge_cy: 200;

// ─── 选中图标(-1 = 未选中)───
property <int> selected_index: 1;

// ─── 点击检测回调(Rust 实现:遍历 icon_data 找最近图标)───
callback find_icon_at(float, float) -> int;

// ─── 布局常量 ───
property <float> clock_cx: 350;
property <float> clock_cy: 200;
property <float> clip_r: 155;

// ─── 图片资源(编译时嵌入)───
property <[image]> image_urls: [
@imageurl("../images/apple.svg"),
@imageurl("../images/avocado.svg"),
@imageurl("../images/banana.svg"),
@imageurl("../images/bayberry.svg"),
@imageurl("../images/blueberry.svg"),
@imageurl("../images/cherry.svg"),
@imageurl("../images/dragonfruit.svg"),
@imageurl("../images/grape.svg"),
@imageurl("../images/mango.svg"),
@imageurl("../images/mangosteen.svg"),
@imageurl("../images/orange.svg"),
@imageurl("../images/peach.svg"),
@imageurl("../images/persimmon.svg"),
@imageurl("../images/pineapple.svg"),
@imageurl("../images/strawberry.svg"),
@imageurl("../images/watermelon.svg"),
@imageurl("../images/waxapple.svg"),
];

// ─── 图标数据(21个六角网格位置,Rust 需要读取以做点击检测)───
in property <[IconData]> icon_data: [
// 第1行 (y=100, 3个)
{ base_x: 290.0, base_y: 100.0, sz: 54.0, icon_index: 0, name: "apple" },
{ base_x: 348.0, base_y: 100.0, sz: 54.0, icon_index: 1, name: "avocado" },
{ base_x: 406.0, base_y: 100.0, sz: 54.0, icon_index: 2, name: "banana" },
// 第2行 (y=150, 5个, 偏移+29)
{ base_x: 205.0, base_y: 150.0, sz: 54.0, icon_index: 3, name: "bayberry" },
{ base_x: 263.0, base_y: 150.0, sz: 54.0, icon_index: 4, name: "blueberry" },
{ base_x: 321.0, base_y: 150.0, sz: 54.0, icon_index: 5, name: "cherry" },
{ base_x: 379.0, base_y: 150.0, sz: 54.0, icon_index: 6, name: "dragonfruit" },
{ base_x: 437.0, base_y: 150.0, sz: 54.0, icon_index: 7, name: "grape" },
// 第3行 (y=200, 5个, 中间行)
{ base_x: 234.0, base_y: 200.0, sz: 54.0, icon_index: 8, name: "mango" },
{ base_x: 292.0, base_y: 200.0, sz: 54.0, icon_index: 9, name: "mangosteen" },
{ base_x: 350.0, base_y: 200.0, sz: 54.0, icon_index: 10, name: "orange" },
{ base_x: 408.0, base_y: 200.0, sz: 54.0, icon_index: 11, name: "peach" },
{ base_x: 466.0, base_y: 200.0, sz: 54.0, icon_index: 12, name: "persimmon" },
// 第4行 (y=250, 5个, 偏移+29)
{ base_x: 205.0, base_y: 250.0, sz: 54.0, icon_index: 13, name: "pineapple" },
{ base_x: 263.0, base_y: 250.0, sz: 54.0, icon_index: 14, name: "strawberry" },
{ base_x: 321.0, base_y: 250.0, sz: 54.0, icon_index: 15, name: "watermelon" },
{ base_x: 379.0, base_y: 250.0, sz: 54.0, icon_index: 16, name: "waxapple" },
{ base_x: 437.0, base_y: 250.0, sz: 54.0, icon_index: 17, name: "apple" },
// 第5行 (y=300, 3个)
{ base_x: 290.0, base_y: 300.0, sz: 54.0, icon_index: 18, name: "avocado" },
{ base_x: 348.0, base_y: 300.0, sz: 54.0, icon_index: 19, name: "banana" },
{ base_x: 406.0, base_y: 300.0, sz: 54.0, icon_index: 20, name: "bayberry" },
];

// ═══════════════════════════════════════════════
// 表盘外框
// ═══════════════════════════════════════════════

Rectangle {
x: root.clock_cx * 1px root.clip_r * 1px 8px;
y: root.clock_cy * 1px root.clip_r * 1px 8px;
width: root.clip_r * 2px + 16px;
height: root.clip_r * 2px + 16px;
borderradius: root.clip_r * 1px + 8px;
background: @lineargradient(145deg, #2e2e2e 0%, #0f0f0f 50%, #1e1e1e 100%);
borderwidth: 1.5px;
bordercolor: #555;
}

Rectangle {
x: root.clock_cx * 1px root.clip_r * 1px 3px;
y: root.clock_cy * 1px root.clip_r * 1px 3px;
width: root.clip_r * 2px + 6px;
height: root.clip_r * 2px + 6px;
borderradius: root.clip_r * 1px + 3px;
background: @lineargradient(200deg, #222 0%, #0a0a0a 50%, #181818 100%);
borderwidth: 1px;
bordercolor: #333;
}

Rectangle {
x: root.clock_cx * 1px root.clip_r * 1px;
y: root.clock_cy * 1px root.clip_r * 1px;
width: root.clip_r * 2px;
height: root.clip_r * 2px;
borderradius: root.clip_r * 1px;
background: #050505;
borderwidth: 0.5px;
bordercolor: #1a1a1a;
}

// ═══════════════════════════════════════════════
// 蜂巢图标网格 + 鱼眼凸起效果
// ═══════════════════════════════════════════════

for icon[idx] in root.icon_data : Rectangle {
property <float> cdx: icon.base_x root.clock_cx;
property <float> cdy: icon.base_y root.clock_cy;
property <float> cdist: Math.sqrt(self.cdx * self.cdx + self.cdy * self.cdy);
property <bool> in_circle: self.cdist <= root.clip_r 14;

property <float> mdx: icon.base_x root.bulge_cx;
property <float> mdy: icon.base_y root.bulge_cy;
property <float> mdist: Math.sqrt(self.mdx * self.mdx + self.mdy * self.mdy);
property <float> bulge: 0.38 + 0.82 * Math.exp((self.mdist * self.mdist) / 5500.0);

property <float> scaled_sz: icon.sz * self.bulge;
property <bool> is_selected: idx == root.selected_index;

visible: self.in_circle;
x: (icon.base_x self.scaled_sz / 2.0) * 1px;
y: (icon.base_y self.scaled_sz / 2.0) * 1px;
width: self.scaled_sz * 1px;
height: self.scaled_sz * 1px;
borderradius: self.scaled_sz * 0.24 * 1px;
background: self.is_selected
? @lineargradient(155deg, #2a3a50 0%, #152030 100%)
: @lineargradient(155deg, #1e1e20 0%, #0c0c0e 100%);
borderwidth: self.is_selected ? 1.5px : 0.5px;
bordercolor: self.is_selected ? #4a8eff80 : #3a3a3a50;

Image {
source: root.image_urls[Math.mod(icon.icon_index, root.image_urls.length)];
width: parent.width * 0.60;
height: parent.height * 0.60;
x: parent.width * 0.20;
y: parent.height * 0.20;
}
}

// ═══════════════════════════════════════════════
// 鼠标交互(凸起跟随 + 点击检测)
// ═══════════════════════════════════════════════

TouchArea {
x: 0;
y: 0;
width: root.width;
height: root.height;
private property <bool> is_pressed:false;

moved => {
root.bulge_cx = self.mousex / 1px;
root.bulge_cy = self.mousey / 1px;
}

clicked => {
// Rust 回调:遍历 icon_data,找距离点击位置最近的图标
/*root.selected_index = root.find_icon_at(
self.mouse-x / 1px,
self.mouse-y / 1px
);*/

}
pointerevent(event) => {
if event.kind == PointerEventKind.down {
self.is_pressed = true;
} else if event.kind == PointerEventKind.move{
self.is_pressed = false;
}
else if event.kind == PointerEventKind.up {
if(self.is_pressed){
// Rust 回调:遍历 icon_data,找距离点击位置最近的图标
root.selected_index = root.find_icon_at(
self.mousex / 1px,
self.mousey / 1px
);
}
self.is_pressed = false;
}
}
}

// ═══════════════════════════════════════════════
// 底部信息面板
// ═══════════════════════════════════════════════

Rectangle {
x: root.clock_cx * 1px 150px;
y: root.clock_cy * 1px + root.clip_r * 1px + 20px;
width: 300px;
height: 46px;
borderradius: 23px;
background: root.selected_index >= 0 ? #1a1a2e : #111118;
borderwidth: 1px;
bordercolor: root.selected_index >= 0 ? #4a8eff40 : #222;

Text {
x: 0;
y: 0;
width: parent.width;
height: parent.height;
horizontalalignment: center;
verticalalignment: center;
text: root.selected_index >= 0
? "📌 #" + root.selected_index + " " + root.icon_data[root.selected_index].name
: "点击图标查看详情";
fontsize: 14px;
fontweight: 300;
color: root.selected_index >= 0 ? #8ab4f8 : #555;
}
}
}

5、Cargo.toml

[package]
name = "modbus-debug-assistant"
version = "0.1.0"
edition = "2024"

[dependencies]
slint = "1.17.1"

[builddependencies]
slintbuild = "1.17.1"

三、实现原理

本项目整体采用 Rust + Slint 前后端分离架构:Slint 负责 UI 渲染与交互,Rust 负责业务逻辑与点击检测。下面从布局、渲染、交互三个层面拆解核心实现。

1、蜂巢网格布局

蜂巢(六边形)网格的本质是错位排列的圆形网格:奇数行与偶数行在水平方向错开半个间距,从而形成六边形的视觉交错感。

在 main.slint 中,21 个图标的位置并非运行时计算,而是预先在 icon_data 数组中硬编码。以第 1 行(y=100)和第 2 行(y=150)为例:

// 第1行 (y=100, 3个)
{ base_x: 290.0, base_y: 100.0, sz: 54.0, icon_index: 0, name: "apple" },
{ base_x: 348.0, base_y: 100.0, sz: 54.0, icon_index: 1, name: "avocado" },
{ base_x: 406.0, base_y: 100.0, sz: 54.0, icon_index: 2, name: "banana" },
// 第2行 (y=150, 5个, 偏移+29)
{ base_x: 205.0, base_y: 150.0, sz: 54.0, icon_index: 3, name: "bayberry" },

可以看到,第 2 行比第 1 行在水平方向偏移了 +29px,这正是蜂巢错位的关键。每行图标数量按 3-5-5-5-3 分布,整体呈菱形,正好贴合圆形表盘的裁剪区域。

2、圆形裁剪与可见性控制

表盘并非把所有图标平铺显示,而是通过圆形裁剪只保留中心区域内的图标。核心逻辑在 for 循环中:

property <float> cdx: icon.base_x – root.clock_cx;
property <float> cdy: icon.base_y – root.clock_cy;
property <float> cdist: Math.sqrt(self.cdx * self.cdx + self.cdy * self.cdy);
property <bool> in_circle: self.cdist <= root.clip_r – 14;

  • cdx / cdy:图标中心到表盘中心 (clock_cx, clock_cy) 的向量;
  • cdist:图标中心到表盘中心的欧几里得距离;
  • in_circle:当距离小于裁剪半径 clip_r – 14 时,图标才可见。

visible: self.in_circle;

这样,位于圆形表盘边缘之外的图标会被自动隐藏,形成干净的圆形表盘效果。

3、鱼眼凸起效果(Fisheye Bulge)

这是整个项目最出彩的视觉效果。当鼠标移动时,靠近鼠标的图标会放大凸起,远离的则缩小,形成类似鱼眼镜头的扭曲感。

核心公式在 for 循环中:

property <float> mdx: icon.base_x – root.bulge_cx;
property <float> mdy: icon.base_y – root.bulge_cy;
property <float> mdist: Math.sqrt(self.mdx * self.mdx + self.mdy * self.mdy);
property <float> bulge: 0.38 + 0.82 * Math.exp(-(self.mdist * self.mdist) / 5500.0);

这里使用了高斯函数(正态分布)作为缩放因子:

  • 当鼠标恰好位于图标中心(mdist = 0)时,bulge = 0.38 + 0.82 * 1.0 = 1.20,图标放大到 120%;
  • 当鼠标远离时,mdist 增大,exp(-mdist²/5500) 趋近于 0,bulge 趋近于 0.38,图标缩小到 38%。

5500 是高斯函数的方差参数,控制凸起的影响范围——数值越大,影响半径越广,过渡越平缓。

最终每个图标的实际尺寸为:

property <float> scaled_sz: icon.sz * self.bulge;

4、鼠标交互与点击检测

凸起跟随

TouchArea 的 moved 事件实时更新凸起中心:

moved => {
root.bulge_cx = self.mouse-x / 1px;
root.bulge_cy = self.mouse-y / 1px;
}

bulge_cx / bulge_cy 变化后,所有图标的 bulge 因子会自动重新计算,因为 Slint 的响应式绑定会自动追踪依赖并触发重绘。

点击检测(Rust 回调)

点击检测采用 Rust 回调 + 最近邻搜索 策略。Slint 侧通过 pointer-event 捕获按下与抬起:

pointer-event(event) => {
if event.kind == PointerEventKind.down {
self.is_pressed = true;
} else if event.kind == PointerEventKind.move{
self.is_pressed = false;
}
else if event.kind == PointerEventKind.up {
if(self.is_pressed){
root.selected_index = root.find_icon_at(
self.mouse-x / 1px,
self.mouse-y / 1px
);
}
self.is_pressed = false;
}
}

这里用 is_pressed 标志区分「按下后抬起」与「拖动后抬起」,避免拖动过程中误触发点击。

Rust 侧实现 find_icon_at 回调,遍历所有图标位置,找到距离点击点最近且在命中半径内的图标:

ui.on_find_icon_at(move |click_x, click_y| -> i32 {
let mut closest_idx: i32 = 1;
let mut closest_dist = f32::MAX;

for (base_x, base_y, sz, icon_index) in &positions_clone {
let dx = *base_x click_x as f32;
let dy = *base_y click_y as f32;
let dist = (dx * dx + dy * dy).sqrt();
let hit_radius = sz * 0.7;

if dist <= hit_radius && dist < closest_dist {
closest_dist = dist;
closest_idx = *icon_index;
}
}

closest_idx
});

  • 命中半径取图标尺寸的 0.7 倍,兼顾点击精度与容错;
  • 采用最近邻策略,即使两个图标相邻,也只会选中距离最近的那个。

5、数据流与响应式更新

整个项目的数据流可以概括为:

#mermaid-svg-KtdjF8m1Uqygaiy5{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-KtdjF8m1Uqygaiy5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-KtdjF8m1Uqygaiy5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-KtdjF8m1Uqygaiy5 .error-icon{fill:#552222;}#mermaid-svg-KtdjF8m1Uqygaiy5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-KtdjF8m1Uqygaiy5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-KtdjF8m1Uqygaiy5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-KtdjF8m1Uqygaiy5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-KtdjF8m1Uqygaiy5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-KtdjF8m1Uqygaiy5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-KtdjF8m1Uqygaiy5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-KtdjF8m1Uqygaiy5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-KtdjF8m1Uqygaiy5 .marker.cross{stroke:#333333;}#mermaid-svg-KtdjF8m1Uqygaiy5 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-KtdjF8m1Uqygaiy5 p{margin:0;}#mermaid-svg-KtdjF8m1Uqygaiy5 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-KtdjF8m1Uqygaiy5 .cluster-label text{fill:#333;}#mermaid-svg-KtdjF8m1Uqygaiy5 .cluster-label span{color:#333;}#mermaid-svg-KtdjF8m1Uqygaiy5 .cluster-label span p{background-color:transparent;}#mermaid-svg-KtdjF8m1Uqygaiy5 .label text,#mermaid-svg-KtdjF8m1Uqygaiy5 span{fill:#333;color:#333;}#mermaid-svg-KtdjF8m1Uqygaiy5 .node rect,#mermaid-svg-KtdjF8m1Uqygaiy5 .node circle,#mermaid-svg-KtdjF8m1Uqygaiy5 .node ellipse,#mermaid-svg-KtdjF8m1Uqygaiy5 .node polygon,#mermaid-svg-KtdjF8m1Uqygaiy5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-KtdjF8m1Uqygaiy5 .rough-node .label text,#mermaid-svg-KtdjF8m1Uqygaiy5 .node .label text,#mermaid-svg-KtdjF8m1Uqygaiy5 .image-shape .label,#mermaid-svg-KtdjF8m1Uqygaiy5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-KtdjF8m1Uqygaiy5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-KtdjF8m1Uqygaiy5 .rough-node .label,#mermaid-svg-KtdjF8m1Uqygaiy5 .node .label,#mermaid-svg-KtdjF8m1Uqygaiy5 .image-shape .label,#mermaid-svg-KtdjF8m1Uqygaiy5 .icon-shape .label{text-align:center;}#mermaid-svg-KtdjF8m1Uqygaiy5 .node.clickable{cursor:pointer;}#mermaid-svg-KtdjF8m1Uqygaiy5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-KtdjF8m1Uqygaiy5 .arrowheadPath{fill:#333333;}#mermaid-svg-KtdjF8m1Uqygaiy5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-KtdjF8m1Uqygaiy5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-KtdjF8m1Uqygaiy5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KtdjF8m1Uqygaiy5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-KtdjF8m1Uqygaiy5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KtdjF8m1Uqygaiy5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-KtdjF8m1Uqygaiy5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-KtdjF8m1Uqygaiy5 .cluster text{fill:#333;}#mermaid-svg-KtdjF8m1Uqygaiy5 .cluster span{color:#333;}#mermaid-svg-KtdjF8m1Uqygaiy5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-KtdjF8m1Uqygaiy5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-KtdjF8m1Uqygaiy5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-KtdjF8m1Uqygaiy5 .icon-shape,#mermaid-svg-KtdjF8m1Uqygaiy5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KtdjF8m1Uqygaiy5 .icon-shape p,#mermaid-svg-KtdjF8m1Uqygaiy5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-KtdjF8m1Uqygaiy5 .icon-shape .label rect,#mermaid-svg-KtdjF8m1Uqygaiy5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KtdjF8m1Uqygaiy5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-KtdjF8m1Uqygaiy5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-KtdjF8m1Uqygaiy5 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

TouchArea 鼠标移动

更新 bulge_cx / bulge_cy

高斯函数计算 bulge 因子

scaled_sz 缩放图标尺寸

pointer-event 抬起事件

Rust find_icon_at 回调

返回最近图标 index

selected_index 更新

底部面板显示图标名称

Slint 的响应式绑定机制保证了:只要 bulge_cx、bulge_cy、selected_index 任一属性变化,所有依赖它们的 UI 元素会自动重绘,无需手动刷新。

6、小结

模块技术要点
蜂巢布局 奇数/偶数行错位排列,预置坐标
圆形裁剪 距离判断 + visible 绑定
鱼眼凸起 高斯函数 exp(-d²/σ²) 缩放
点击检测 Rust 最近邻搜索 + 命中半径
响应式更新 Slint 属性绑定自动重绘

通过 Rust 与 Slint 的配合,既保留了 Slint 声明式 UI 的高效开发体验,又借助 Rust 实现了高性能的几何计算,整体架构清晰、职责分明。

在这里插入图片描述

赞(0)
未经允许不得转载:171主机测评 » Rust+Slint 实现蜂巢菜单源码分享
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址