use std::rc::Rc;
use lieui::prelude::*;
use lieui_layout::FlexAlign;
struct Wave {
taps: u32,
}
impl CustomNode for Wave {
fn intrinsic_size(&self) -> Size {
Size::new(300.0, 44.0)
}
fn draw(&self, out: &mut Scene, rect: Rect, tr: Affine) {
let n = 24usize;
let w = rect.width / n as f32;
for i in 0..n {
let t = i as f32 / n as f32;
let s = (t * 6.0 + self.taps as f32 * 0.9).sin() * 0.5 + 0.5;
let h = rect.height * (0.2 + 0.8 * s);
custom::fill_rect(
out,
Rect::new(rect.x + i as f32 * w, rect.bottom() - h, w * 0.7, h),
1.5,
Color::new(80, 140, 240),
tr,
);
}
}
fn on_event(&mut self, id: NodeId, ev: &EventView, cmd: &mut CmdBuf) -> bool {
if ev.kind == EventKind::Tapped {
self.taps += 1;
cmd.damage(id);
true
} else {
false
}
}
}
struct Gallery {
rt: Runtime,
clicks: Signal<i32>,
volume: Signal<f32>,
agree: Signal<bool>,
flavor: Signal<String>,
name: Signal<String>,
combo_open: Signal<bool>,
combo_choice: Signal<String>,
open_menu: Signal<String>,
sub_open: Signal<bool>,
last_action: Signal<String>,
wave: CustomCell,
photo: std::sync::Arc<ImageData>,
}
impl Gallery {
fn menu_anchor(me: &Rc<Self>, v: &mut ViewBuf, name: &str) {
let m = name.to_string();
v.button(name)
.key(format!("menu-{name}"))
.on_tap(act1(me, |s, m: String| {
s.sub_open.set(false); s.open_menu
.set(if s.open_menu.get() == m { String::new() } else { m });
}, m));
}
fn menu_popup(me: &Rc<Self>, v: &mut ViewBuf, name: &str, items: &[&str]) {
if me.open_menu.get() != name {
return;
}
let key = format!("menu-{name}");
v.popup_at(key.as_str(), Placement::Below, |p| {
let mine = Rc::clone(me);
let owned = name.to_string();
p.on(EventKind::Dismissed, move |_| {
if mine.open_menu.get() == owned {
mine.open_menu.set(String::new());
mine.sub_open.set(false);
}
});
for item in items {
let it = Rc::clone(me);
let label = item.to_string();
if item.ends_with("▸") {
let sk = format!("item-{name}-{item}");
p.text(item.to_string())
.hover_background(me.rt.theme().control_hover)
.radius(3.0)
.width(110.0)
.key(sk)
.on_tap(act(&it, |s| s.sub_open.set(!s.sub_open.get())));
continue;
}
p.text(item.to_string())
.hover_background(me.rt.theme().control_hover)
.radius(3.0)
.width(110.0)
.on_tap(move || {
it.last_action.set(label.clone());
it.open_menu.set(String::new());
});
}
});
}
fn submenu_popup(me: &Rc<Self>, v: &mut ViewBuf, menu: &str, item: &str, subs: &[&str]) {
if me.open_menu.get() != menu || !me.sub_open.get() {
return;
}
let key = format!("item-{menu}-{item}");
v.popup_at(key.as_str(), Placement::RightOf, |p| {
for sub in subs {
let it = Rc::clone(me);
let label = sub.to_string();
p.text(sub.to_string())
.hover_background(me.rt.theme().control_hover)
.radius(3.0)
.width(110.0)
.on_tap(move || {
it.last_action.set(label.clone());
it.open_menu.set(String::new());
it.sub_open.set(false);
});
}
});
}
fn combo_anchor(me: &Rc<Self>, v: &mut ViewBuf) {
let label = if me.combo_choice.get().is_empty() {
"下拉选择…".to_string()
} else {
format!("已选:{}", me.combo_choice.get())
};
v.button(&label)
.key("combo-btn")
.width(150.0)
.on_tap(act(me, |s| s.combo_open.set(!s.combo_open.get())));
}
fn combo_popup(me: &Rc<Self>, v: &mut ViewBuf) {
if !me.combo_open.get() {
return;
}
v.popup_at("combo-btn", Placement::Below, |p| {
let mine = Rc::clone(me);
p.on(EventKind::Dismissed, move |_| mine.combo_open.set(false));
for opt in ["樱桃", "猕猴桃", "水蜜桃"] {
let it = Rc::clone(me);
let label = opt.to_string();
p.text(opt.to_string())
.hover_background(me.rt.theme().control_hover)
.radius(3.0)
.padding(6.0)
.width(150.0)
.on_tap(move || {
it.combo_choice.set(label.clone());
it.combo_open.set(false);
});
}
});
}
fn divider(&self, v: &mut ViewBuf) {
v.row(|r| {
r.height(1.0);
r.background(self.rt.theme().control_border);
});
}
}
impl ViewModel for Gallery {
fn view(self: &Rc<Self>, v: &mut ViewBuf) {
v.column(|c| {
c.row(|r| {
r.height(30.0);
r.gap(0.0);
r.align_items(FlexAlign::Center);
Gallery::menu_anchor(self, r, "文件");
Gallery::menu_anchor(self, r, "编辑");
r.spacer();
r.text(format!("最近操作:{}", {
let l = self.last_action.get();
if l.is_empty() { "(无)".to_string() } else { l }
}))
.font_size(13.0)
.color(self.rt.theme().text_secondary);
});
c.scroll(|s| {
s.expand(true);
s.align_items(FlexAlign::Center);
s.column(|s| {
s.width(360.0);
s.gap(16.0);
s.text("组件陈列馆").font_size(22.0);
s.row(|r| {
r.gap(10.0);
r.button("切换深色主题").on_tap(act(self, |s| {
if s.rt.theme() == Theme::dark() {
s.rt.set_theme(Theme::light());
} else {
s.rt.set_theme(Theme::dark());
}
}));
r.text("或点这里看看 hover").font_size(13.0);
});
self.divider(s);
s.text("按钮").font_size(16.0);
s.row(|r| {
r.gap(10.0);
r.button(&format!("点了我 {} 次", self.clicks.get()))
.on_tap(act(self, |s| s.clicks.update(|v| *v += 1)));
r.button("归零").on_tap(act(self, |s| s.clicks.set(0)));
});
self.divider(s);
s.text(format!("音量 {:.1} / 10(拖我)", self.volume.get())).font_size(16.0);
s.slider_bound(&self.volume, 0.0, 10.0);
s.progress(self.volume.get() / 10.0);
self.divider(s);
s.text("选择控件(复选框与开关共享同一个值)").font_size(16.0);
s.row(|r| {
r.gap(10.0);
r.checkbox_bound(&self.agree);
r.switch_bound(&self.agree);
r.text(if self.agree.get() { "已启用" } else { "已禁用" }).font_size(14.0);
});
s.row(|r| {
r.gap(10.0);
r.radio_bound(&self.flavor, "apple");
r.text("苹果").font_size(14.0);
r.radio_bound(&self.flavor, "banana");
r.text("香蕉").font_size(14.0);
r.radio_bound(&self.flavor, "cherry");
r.text("樱桃").font_size(14.0);
});
self.divider(s);
s.text("下拉选择").font_size(16.0);
Gallery::combo_anchor(self, s);
self.divider(s);
s.text("图标(icon / icon_button)").font_size(16.0);
s.row(|r| {
r.gap(6.0);
r.align_items(FlexAlign::Center);
for name in ["home", "search", "settings", "add", "delete", "close", "refresh"] {
r.icon_button(name)
.on_tap(act1(self, |s, n: String| s.last_action.set(format!("图标 {n}")), name.to_string()));
}
});
s.row(|r| {
r.gap(16.0);
r.align_items(FlexAlign::Center);
r.icon("favorite").color(self.rt.theme().accent).font_size(28.0);
r.icon("star").font_size(24.0);
r.icon("info").font_size(20.0);
r.icon("menu").font_size(16.0);
r.text("不同字号(28/24/20/16),可 .color 变色")
.font_size(13.0)
.color(self.rt.theme().text_secondary);
});
self.divider(s);
s.text("输入框(中文 IME / Ctrl+A·C·V / 方向键选区)").font_size(16.0);
s.input_bound(&self.name).placeholder("在这里输入…");
s.text(format!("正在输入:{}", self.name.get())).font_size(14.0);
self.divider(s);
s.text("图片(程序生成,contain 缩放)").font_size(16.0);
s.image(self.photo.clone()).width(240.0).height(120.0);
self.divider(s);
s.text("自绘波形(CustomNode,点它)").font_size(16.0);
s.custom(&self.wave);
self.divider(s);
s.text("嵌套滚动区").font_size(16.0);
s.scroll(|inner| {
inner.width(320.0);
inner.height(140.0);
inner.gap(6.0);
inner.align_items(FlexAlign::Start);
for i in 1..=16 {
inner.text(format!("内滚内容第 {i} 行 —— 滚轮或拖右侧滚动条"));
}
});
s.text("Tab 可在按钮 / 滑块 / 复选框 / 开关 / 单选 / 输入框之间移动焦点").font_size(13.0);
});
});
});
Gallery::menu_popup(self, v, "文件", &["新建", "打开", "保存"]);
Gallery::menu_popup(self, v, "编辑", &["撤销", "重做", "查找 ▸"]);
Gallery::submenu_popup(self, v, "编辑", "查找 ▸", &["查找内容", "查找下一个", "替换…"]);
Gallery::combo_popup(self, v);
}
}
fn demo_image() -> ImageData {
let (w, h) = (64u32, 64u32);
let mut rgba = Vec::with_capacity((w * h * 4) as usize);
for y in 0..h {
for x in 0..w {
let dx = x as f32 - 32.0;
let dy = y as f32 - 32.0;
let d = (dx * dx + dy * dy).sqrt();
let ring = ((d - 18.0).abs() < 6.0) as u8 * 255;
rgba.push((x * 3 + ring as u32) as u8);
rgba.push((y * 3) as u8);
rgba.push(200);
rgba.push(255);
}
}
ImageData { width: w, height: h, rgba }
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let rt = Runtime::new();
let mut app = App::new(rt.clone());
let vm = Rc::new(Gallery {
clicks: Signal::new(&rt, 0),
volume: Signal::new(&rt, 3.0),
agree: Signal::new(&rt, false),
flavor: Signal::new(&rt, "apple".to_string()),
name: Signal::new(&rt, String::new()),
combo_open: Signal::new(&rt, false),
combo_choice: Signal::new(&rt, String::new()),
open_menu: Signal::new(&rt, String::new()),
sub_open: Signal::new(&rt, false),
last_action: Signal::new(&rt, String::new()),
wave: custom::cell(Wave { taps: 0 }),
rt: rt.clone(),
photo: std::sync::Arc::new(demo_image()),
});
let id = app.window_erased(
WindowConfig::new()
.title("lieui v3 · 组件陈列馆")
.size(480.0, 640.0)
.min_size(380.0, 420.0),
lieui::app::erased(Rc::clone(&vm)),
);
let _ = id;
app.run()
}