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

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

一、效果展示

二、源码分享

1、工程结构

2、main.rs

rust 复制代码
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

rust 复制代码
fn main(){
    slint_build::compile("ui/main.slint")
        .unwrap()
}

4、main.slint

rust 复制代码
export struct IconData {
    base_x: float,
    base_y: float,
    sz: float,
    icon_index: int,
    name: string,
}

export component MainWindow inherits Window {
    title: "蜂巢菜单";
    preferred-width: 700px;
    preferred-height: 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: [
        @image-url("../images/apple.svg"),
        @image-url("../images/avocado.svg"),
        @image-url("../images/banana.svg"),
        @image-url("../images/bayberry.svg"),
        @image-url("../images/blueberry.svg"),
        @image-url("../images/cherry.svg"),
        @image-url("../images/dragonfruit.svg"),
        @image-url("../images/grape.svg"),
        @image-url("../images/mango.svg"),
        @image-url("../images/mangosteen.svg"),
        @image-url("../images/orange.svg"),
        @image-url("../images/peach.svg"),
        @image-url("../images/persimmon.svg"),
        @image-url("../images/pineapple.svg"),
        @image-url("../images/strawberry.svg"),
        @image-url("../images/watermelon.svg"),
        @image-url("../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;
        border-radius: root.clip_r * 1px + 8px;
        background: @linear-gradient(145deg, #2e2e2e 0%, #0f0f0f 50%, #1e1e1e 100%);
        border-width: 1.5px;
        border-color: #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;
        border-radius: root.clip_r * 1px + 3px;
        background: @linear-gradient(200deg, #222 0%, #0a0a0a 50%, #181818 100%);
        border-width: 1px;
        border-color: #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;
        border-radius: root.clip_r * 1px;
        background: #050505;
        border-width: 0.5px;
        border-color: #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;
        border-radius: self.scaled_sz * 0.24 * 1px;
        background: self.is_selected
            ? @linear-gradient(155deg, #2a3a50 0%, #152030 100%)
            : @linear-gradient(155deg, #1e1e20 0%, #0c0c0e 100%);
        border-width: self.is_selected ? 1.5px : 0.5px;
        border-color: 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.mouse-x / 1px;
            root.bulge_cy = self.mouse-y / 1px;
        }

        clicked => {
            // Rust 回调:遍历 icon_data,找距离点击位置最近的图标
            /*root.selected_index = root.find_icon_at(
                self.mouse-x / 1px,
                self.mouse-y / 1px
            );*/
        }
        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){
                    // Rust 回调:遍历 icon_data,找距离点击位置最近的图标
                    root.selected_index = root.find_icon_at(
                        self.mouse-x / 1px,
                        self.mouse-y / 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;
        border-radius: 23px;
        background: root.selected_index >= 0 ? #1a1a2e : #111118;
        border-width: 1px;
        border-color: root.selected_index >= 0 ? #4a8eff40 : #222;

        Text {
            x: 0;
            y: 0;
            width: parent.width;
            height: parent.height;
            horizontal-alignment: center;
            vertical-alignment: center;
            text: root.selected_index >= 0
                ? "📌 #" + root.selected_index + "  " + root.icon_data[root.selected_index].name
                : "点击图标查看详情";
            font-size: 14px;
            font-weight: 300;
            color: root.selected_index >= 0 ? #8ab4f8 : #555;
        }
    }
}

5、Cargo.toml

rust 复制代码
[package]
name = "modbus-debug-assistant"
version = "0.1.0"
edition = "2024"

[dependencies]
slint = "1.17.1"


[build-dependencies]
slint-build = "1.17.1"

三、实现原理

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

1、蜂巢网格布局

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

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

slint 复制代码
// 第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 循环中:

slint 复制代码
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 时,图标才可见。
slint 复制代码
visible: self.in_circle;

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

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

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

核心公式在 for 循环中:

slint 复制代码
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 是高斯函数的方差参数,控制凸起的影响范围------数值越大,影响半径越广,过渡越平缓。

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

slint 复制代码
property <float> scaled_sz: icon.sz * self.bulge;

4、鼠标交互与点击检测

凸起跟随

TouchAreamoved 事件实时更新凸起中心:

slint 复制代码
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 捕获按下与抬起:

slint 复制代码
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 回调,遍历所有图标位置,找到距离点击点最近且在命中半径内的图标:

rust 复制代码
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_cxbulge_cyselected_index 任一属性变化,所有依赖它们的 UI 元素会自动重绘,无需手动刷新。

6、小结

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

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

相关推荐
我命由我1234516 分钟前
JS Mock(Mock 拦截请求的前提、Mock 使用、关闭 Mock)
开发语言·前端·javascript·前端框架·html·html5·js
OPEN-F23 分钟前
C++并发编程:异步任务与线程池实战
开发语言·c++·算法
心中有你021441 分钟前
Python爬虫实战:京东商品数据爬取+可视化分析
开发语言·爬虫·python
不正经学生6 小时前
C语言预处理详解:编译器真正动手之前的那些事
c语言·开发语言·算法·面试·bug
门思科技6 小时前
LoRaWAN 设备类别:Class A、B、C 对比与选型指南
c语言·开发语言·php
恋恋西风9 小时前
C++ 理解 std::thread 在单核和多核上的行为差异
开发语言·c++
Brilliantwxx9 小时前
【Linux】 进程(9)程序与进程地址空间(基础+进阶+面试题)
linux·运维·服务器·开发语言·c++
mldong9 小时前
流程实例如何沿着箭头走到结束:Rust 工作流引擎的执行内核
架构·rust
BizzZ_10 小时前
C++(22)——类型转换和IO流
开发语言·c++