Browse Source

创建toolkit库 实现基本的框架和部分控件 (#4)

R0ronoa 11 months ago
parent
commit
45e1715779

+ 1 - 0
.vscode/settings.json

@@ -2,5 +2,6 @@
     "rust-analyzer.linkedProjects": [
         "./starry_client/Cargo.toml",
         "./starry_server/Cargo.toml",
+        "./starry_toolkit/Cargo.toml",
     ],
 }

+ 3 - 2
starry_client/src/base/renderer.rs

@@ -7,6 +7,7 @@ use super::{
 
 static FONT_ASSET : &[u8] = include_bytes!("../font/unifont.font");
 
+/// 渲染模式: 混合/覆盖
 #[derive(Clone, Copy, Debug)]
 pub enum RenderMode {
     /// 颜色混合
@@ -15,7 +16,7 @@ pub enum RenderMode {
     Overwrite,
 }
 
-/// 用于进行渲染
+/// 可渲染对象需要实现的特性
 pub trait Renderer {
     /// 获取渲染窗口宽度
     fn width(&self) -> u32;
@@ -29,7 +30,7 @@ pub trait Renderer {
     /// 获取可变帧缓存数据
     fn data_mut(&mut self) -> &mut [Color];
 
-    /// 同步数据
+    /// 同步渲染数据
     fn sync(&mut self) -> bool;
 
     /// 获取/设置渲染模式

+ 34 - 20
starry_client/src/window.rs

@@ -9,6 +9,8 @@ const SCREEN_WIDTH: usize = 1440;
 #[allow(dead_code)]
 const SCREEN_HEIGHT: usize = 900;
 
+/// 客户端的窗口类,与服务端的窗口对象一一对应  
+/// 一般来说客户端应用程序不直接使用该类,而通过Toolkit库间接使用
 #[allow(dead_code)]
 pub struct Window {
     /// 窗口左上角的x坐标
@@ -20,7 +22,7 @@ pub struct Window {
     /// 窗口的高度
     h: u32,
     /// 窗口的标题
-    t: String,
+    title: String,
     /// TODO
     // window_async: bool,
     /// 窗口是否大小可变
@@ -52,8 +54,18 @@ impl Renderer for Window {
         self.data_opt.as_mut().unwrap()
     }
 
-    /// TODO
     fn sync(&mut self) -> bool {
+        let mut fb = File::open("/dev/fb0").expect("Unable to open framebuffer");
+
+        for y in 0..self.height() as i32 {
+            for x in 0..self.width() as i32 {
+                let pixel = self.get_pixel(x, y);
+                let offset =  (((y + self.y()) * SCREEN_WIDTH as i32) + x + self.x()) * 4;
+                // 写缓冲区
+                fb.seek(SeekFrom::Start(offset as u64)).expect("Unable to seek framebuffer");
+                fb.write_all(&pixel.to_bgra_bytes()).expect("Unable to write framebuffer");
+            }
+        }
         true
     }
 
@@ -71,7 +83,7 @@ impl Window {
             y: y,
             w: w,
             h: h,
-            t: title.to_string(),
+            title: title.to_string(),
             // window_async: false,
             resizable: false,
             mode: Cell::new(RenderMode::Blend),
@@ -82,33 +94,35 @@ impl Window {
         // TODO: 与服务器通信 
     }
 
-    /// # 函数功能
-    /// 同步数据至系统帧缓冲
-    pub fn sync(&self) {
-        let mut fb = File::open("/dev/fb0").expect("Unable to open framebuffer");
-
-        for y in 0..self.height() as i32 {
-            for x in 0..self.width() as i32 {
-                let pixel = self.get_pixel(x, y);
-                let offset =  (((y + self.y()) * SCREEN_WIDTH as i32) + x + self.x()) * 4;
-                // 写缓冲区
-                fb.seek(SeekFrom::Start(offset as u64)).expect("Unable to seek framebuffer");
-                fb.write_all(&pixel.to_rgba_bytes()).expect("Unable to write framebuffer");
-            }
-        }
-    }
-    
+    /// 返回窗口x坐标
     pub fn x(&self) -> i32 {
         self.x
     }
 
+    /// 返回窗口y坐标
     pub fn y(&self) -> i32 {
         self.y
     }
 
+    /// 返回窗口标题
     pub fn title(&self) -> String {
-        self.t.clone()
+        self.title.clone()
+    }
+
+    /// 改变窗口的位置
+    pub fn set_pos(&mut self, x: i32, y: i32) {
+        self.x = x;
+        self.y = y;
     }
 
+    /// 改变窗口的大小
+    pub fn set_size(&mut self, width: u32, height: u32) {
+        self.w = width;
+        self.h = height;
+    }
 
+    /// 改变窗口标题
+    pub fn set_title(&mut self, title: &str) {
+        self.title = title.to_string();
+    }
 }

+ 2 - 1
starry_server/Cargo.toml

@@ -14,4 +14,5 @@ serde = "1"
 serde_derive = "1"
 toml = "0.8.1"
 log = "0.4.20"
-image = "0.24.7"
+image = "0.24.7"
+resize = "0.3.1"

+ 0 - 0
starry_server/src/cursor_normal.png → starry_server/src/asset/cursor_normal.png


+ 0 - 0
starry_server/src/desktop_bg.png → starry_server/src/asset/desktop_bg.png


+ 24 - 2
starry_server/src/base/image.rs

@@ -3,10 +3,11 @@ use std::{
     cmp,
     fs::File,
     io::{Seek, SeekFrom, Write},
-    mem, ptr,
+    mem, ptr, slice,
 };
 
 use image::GenericImageView;
+use resize::Type;
 use starry_client::base::{
     color::Color,
     renderer::{RenderMode, Renderer},
@@ -270,7 +271,7 @@ impl Image {
         }
     }
 
-    pub fn from_path_scale(path: &[u8]) -> Option<Self> {
+    pub fn from_path(path: &[u8]) -> Option<Self> {
         if let Ok(mut img) = image::load_from_memory(path) {
             // let img = img.resize(20, 20, image::imageops::FilterType::Gaussian);
 
@@ -363,6 +364,27 @@ impl Image {
             }
         }
     }
+
+    /// 改变图像大小
+    pub fn resize(&self, w: u32, h: u32, resize_type: Type) -> Self {
+        let mut dst_color = vec![Color { data: 0 }; w as usize * h as usize].into_boxed_slice();
+
+        let src = unsafe {
+            slice::from_raw_parts(self.data.as_ptr() as *const u8, self.data.len() * 4)
+        };
+
+        let mut dst = unsafe {
+            slice::from_raw_parts_mut(dst_color.as_mut_ptr() as *mut u8, dst_color.len() * 4)
+        };
+
+        let mut resizer = resize::new(self.w as usize, self.h as usize,
+                                      w as usize, h as usize,
+                                      resize::Pixel::RGBA, resize_type);
+        resizer.resize(&src, &mut dst);
+
+        Image::from_data(w as i32, h as i32, dst_color)
+    }
+    
 }
 
 impl Renderer for Image {

+ 2 - 2
starry_server/src/base/window.rs

@@ -15,8 +15,8 @@ pub enum WindowZOrderMode {
     Front,
 }
 
+/// 服务端的窗口类,与客户端的窗口类一一对应    
 #[allow(dead_code)]
-/// 窗口对象
 pub struct Window {
     /// 窗口左上角x坐标
     pub x: i32,
@@ -56,7 +56,7 @@ impl Window {
             title: String::new(),
             transparent: false,
             zorder: WindowZOrderMode::Normal,
-            image: Image::from_path_scale(image_path)
+            image: Image::from_path(image_path)
                 .unwrap_or(Image::new(SCREEN_HEIGHT as i32, SCREEN_HEIGHT as i32)),
             events: Vec::new(),
         }

+ 3 - 3
starry_server/src/core/mod.rs

@@ -18,8 +18,8 @@ pub const SCREEN_WIDTH: usize = 1440;
 #[allow(dead_code)]
 pub const SCREEN_HEIGHT: usize = 900;
 
-static DESKTOP_BG: &[u8] = include_bytes!("../desktop_bg.png");
-static CURSOR_NORMAL: &[u8] = include_bytes!("../cursor_normal.png");
+static DESKTOP_BG: &[u8] = include_bytes!("../asset/desktop_bg.png");
+static CURSOR_NORMAL: &[u8] = include_bytes!("../asset/cursor_normal.png");
 
 static mut STARRY_SERVER: Option<Arc<StarryServer>> = None;
 
@@ -45,7 +45,7 @@ impl StarryServer {
     pub fn new(config: Rc<Config>, displays: Vec<Display>){
         let mut cursors = BTreeMap::new();
         cursors.insert(CursorKind::None, Image::new(0, 0));
-        cursors.insert(CursorKind::Normal, Image::from_path_scale(CURSOR_NORMAL).unwrap_or(Image::new(10, 10)));        // cursors.insert(CursorKind::BottomLeftCorner, Image::from_path_scale(&config.bottom_left_corner, scale).unwrap_or(Image::new(0, 0)));
+        cursors.insert(CursorKind::Normal, Image::from_path(CURSOR_NORMAL).unwrap_or(Image::new(10, 10)));        // cursors.insert(CursorKind::BottomLeftCorner, Image::from_path_scale(&config.bottom_left_corner, scale).unwrap_or(Image::new(0, 0)));
         // cursors.insert(CursorKind::BottomRightCorner, Image::from_path_scale(&config.bottom_right_corner, scale).unwrap_or(Image::new(0, 0)));
         // cursors.insert(CursorKind::BottomSide, Image::from_path_scale(&config.bottom_side, scale).unwrap_or(Image::new(0, 0)));
         // cursors.insert(CursorKind::LeftSide, Image::from_path_scale(&config.left_side, scale).unwrap_or(Image::new(0, 0)));

+ 2 - 1
starry_server/src/lib.rs

@@ -2,4 +2,5 @@ pub mod base;
 pub mod core;
 pub mod config;
 
-extern crate bitflags;
+extern crate bitflags;
+extern crate resize;

+ 2 - 0
starry_toolkit/.cargo/config.toml

@@ -0,0 +1,2 @@
+[build]
+target = "x86_64-unknown-linux-musl"

+ 12 - 0
starry_toolkit/Cargo.toml

@@ -0,0 +1,12 @@
+[package]
+name = "starry_toolkit"
+version = "0.1.0"
+edition = "2021"
+description = "The UI-Toolkit Library of Starry Engine"
+authors = [ "2447742618 <[email protected]>" ]
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+starry_client = {path = "../starry_client" }
+starry_server = {path = "../starry_server" }

+ 41 - 0
starry_toolkit/Makefile

@@ -0,0 +1,41 @@
+# The toolchain we use.
+# You can get it by running DragonOS' `tools/bootstrap.sh`
+TOOLCHAIN="+nightly-2023-08-15-x86_64-unknown-linux_dragonos-gnu"
+RUSTFLAGS+="-C target-feature=+crt-static -C link-arg=-no-pie"
+
+# 如果是在dadk中编译,那么安装到dadk的安装目录中
+INSTALL_DIR?=$(DADK_CURRENT_BUILD_DIR)
+# 如果是在本地编译,那么安装到当前目录下的install目录中
+INSTALL_DIR?=./install
+
+
+run:
+	RUSTFLAGS=$(RUSTFLAGS) cargo $(TOOLCHAIN) run
+
+build:
+	RUSTFLAGS=$(RUSTFLAGS) cargo $(TOOLCHAIN) build
+
+clean:
+	RUSTFLAGS=$(RUSTFLAGS) cargo $(TOOLCHAIN) clean
+
+test:
+	RUSTFLAGS=$(RUSTFLAGS) cargo $(TOOLCHAIN) test
+
+doc:
+	RUSTFLAGS=$(RUSTFLAGS) cargo $(TOOLCHAIN) doc
+
+run-release:
+	RUSTFLAGS=$(RUSTFLAGS) cargo $(TOOLCHAIN) run --release
+
+build-release:
+	RUSTFLAGS=$(RUSTFLAGS) cargo $(TOOLCHAIN) build --release
+
+clean-release:
+	RUSTFLAGS=$(RUSTFLAGS) cargo $(TOOLCHAIN) clean --release
+
+test-release:
+	RUSTFLAGS=$(RUSTFLAGS) cargo $(TOOLCHAIN) test --release
+
+.PHONY: install
+install: 
+	RUSTFLAGS=$(RUSTFLAGS) cargo $(TOOLCHAIN) install --path . --no-track --root $(INSTALL_DIR) --force

BIN
starry_toolkit/src/asset/desktop_bg.png


+ 3 - 0
starry_toolkit/src/base/mod.rs

@@ -0,0 +1,3 @@
+pub mod panel;
+pub mod point;
+pub mod rect;

+ 176 - 0
starry_toolkit/src/base/panel.rs

@@ -0,0 +1,176 @@
+
+use std::{cell::{Cell, RefCell}, sync::Arc};
+
+use starry_client::{base::{color::Color, renderer::{RenderMode, Renderer}}, window::Window};
+
+use crate::widgets::Widget;
+
+use super::rect::Rect;
+
+/// 面板渲染器
+pub struct PanelRenderer<'a> {
+    /// 客户端窗口
+    window: & 'a mut Window,
+}
+
+impl<'a> PanelRenderer<'a> {
+    pub fn new(window: &'a mut Window) -> Self{
+        PanelRenderer {
+            window,
+        }
+    }
+}
+
+impl<'a> Renderer for PanelRenderer<'a> {
+    fn width(&self) -> u32 {
+        self.window.width()
+    }
+
+    fn height(&self) -> u32 {
+        self.window.height()
+    }
+
+    fn data(&self) -> &[Color] {
+        self.window.data()
+    }
+
+    fn data_mut(&mut self) -> &mut [Color] {
+        self.window.data_mut()
+    }
+
+    fn sync(&mut self) -> bool {
+        self.window.sync()
+    }
+
+    fn mode(&self) -> &Cell<RenderMode> {
+        &self.window.mode()
+    }
+
+    // TODO 
+    // fn char(&mut self, x: i32, y: i32, c: char, color: Color) {
+    // }
+}
+
+impl<'a> Drop for PanelRenderer<'a> {
+    fn drop(&mut self) {
+        self.window.sync();
+    }
+}
+
+/// UI面板类作为容器管理一组UI组件(UI-Widget)  
+/// 拥有一个窗口对象用于渲染和事件传递
+pub struct Panel {
+    /// 客户端窗口对象
+    window: RefCell<Window>,
+    /// 子组件数组
+    pub widgets: RefCell<Vec<Arc<dyn Widget>>>,
+    /// 窗口是否打开
+    pub running: Cell<bool>,
+}
+
+impl Panel {
+    pub fn new(rect: Rect, title: &str) -> Self {
+        Panel::from_window(
+            Window::new(rect.x, rect.y, rect.width, rect.height, title)
+        )
+    }
+
+    pub fn from_window(window: Window) -> Self {
+        Panel {
+            window: RefCell::new(window),
+            widgets: RefCell::new(Vec::new()),
+            running: Cell::new(true),
+        }
+    }
+
+    /// 获得客户端窗口对象
+    pub fn into_window(self) -> Window {
+        self.window.into_inner()
+    }
+
+    /// 返回x坐标
+    pub fn x(&self) -> i32 {
+        let window = self.window.borrow();
+        (*window).x()
+    }
+
+    /// 返回y坐标
+    pub fn y(&self) -> i32 {
+        let window = self.window.borrow();
+        (*window).y()
+    }
+
+    /// 返回宽度值
+    pub fn width(&self) -> u32 {
+        let window = self.window.borrow();
+        (*window).width()
+    }
+
+    /// 返回高度值
+    pub fn height(&self) -> u32 {
+        let window = self.window.borrow();
+        (*window).height()
+    }
+
+    /// 窗口标题
+    pub fn title(&self) -> String {
+        let window = self.window.borrow();
+        (*window).title()
+    }
+
+    /// 改变窗口位置
+    pub fn set_pos(&self, x: i32, y: i32) {
+        let mut window = self.window.borrow_mut();
+        (*window).set_pos(x, y);
+    }
+
+    /// 改变窗口大小
+    pub fn set_size(&self, width: u32, height: u32) {
+        let mut window = self.window.borrow_mut();
+        (*window).set_size(width, height);
+    }
+
+    /// 改变窗口标题
+    pub fn set_title(&self, title: &str) {
+        let mut window = self.window.borrow_mut();
+        (*window).set_title(title);
+    }
+
+    /// 关闭窗口
+    pub fn close(&self) {
+        self.running.set(false);
+    }
+
+    /// 添加子组件,返回子组件id
+    pub fn add_child<T: Widget>(&self, widget: &Arc<T>) -> usize {
+        let mut widgets = self.widgets.borrow_mut();
+        let id = widgets.len();
+        widgets.push(widget.clone());
+
+        return id;
+    }
+
+    /// 渲染面板(渲染子组件数组)
+    pub fn draw(&self) {
+        let mut window = self.window.borrow_mut();
+        let mut renderer = PanelRenderer::new(&mut window);
+
+        for widget in self.widgets.borrow().iter() {
+            self.draw_widget(&mut renderer, widget);
+        }
+
+        renderer.sync();
+    }
+
+    /// 渲染单个组件
+    pub fn draw_widget(&self, renderer: &mut dyn Renderer, widget: &Arc<dyn Widget>) {
+        widget.update();
+        widget.draw(renderer);
+
+        // 渲染子组件
+        for child in widget.children().borrow().iter() {
+            self.draw_widget(renderer, child);
+        }
+    }
+
+}

+ 38 - 0
starry_toolkit/src/base/point.rs

@@ -0,0 +1,38 @@
+use std::ops::{Add, Sub};
+
+/// 表示一个位置点
+#[derive(Copy, Clone, Debug, Default)]
+pub struct Point {
+    /// x坐标
+    pub x: i32,
+    /// y坐标
+    pub y: i32,
+}
+
+impl Point {
+    pub fn new(x: i32, y: i32) -> Self {
+        Point { x: x, y: y }
+    }
+}
+
+impl Add for Point {
+    type Output = Point;
+
+    fn add(self, other: Point) -> Self::Output {
+        Point {
+            x: self.x + other.x,
+            y: self.y + other.y,
+        }
+    }
+}
+
+impl Sub for Point {
+    type Output = Point;
+
+    fn sub(self, other: Point) -> Self::Output {
+        Point {
+            x: self.x - other.x,
+            y: self.y - other.y,
+        }
+    }
+}

+ 53 - 0
starry_toolkit/src/base/rect.rs

@@ -0,0 +1,53 @@
+use super::point::Point;
+
+/// 表示一个矩形区域
+#[derive(Clone, Copy, Debug, Default)]
+pub struct Rect {
+    /// 左上角x坐标
+    pub x: i32,
+    /// 左上角y坐标
+    pub y: i32,
+    /// 矩形宽度
+    pub width: u32,
+    /// 矩形高度
+    pub height: u32,
+}
+
+impl Rect {
+    pub fn new(x: i32, y: i32, width: u32, height: u32) -> Rect {
+        Rect {
+            x: x,
+            y: y,
+            width: width,
+            height: height,
+        }
+    }
+
+    /// 返回矩形左上角的位置点
+    pub fn point(&self) -> Point {
+        Point::new(self.x, self.y)
+    }
+
+    /// 判断该矩形是否包含某点
+    pub fn contains(&self, p: Point) -> bool {
+        p.x >= self.x
+            && p.x < self.x + self.width as i32
+            && p.y >= self.y
+            && p.y < self.y + self.height as i32
+    }
+
+    /// 判断该矩形是否完全包含另一个矩形
+    pub fn contains_rect(&self, r: &Rect) -> bool {
+        let p1 = r.point();
+        let p2 = p1 + Point::new(r.width as i32, r.height as i32);
+        self.contains(p1) && self.contains(p2)
+    }
+
+    // 判断该矩形是否和另一矩形有重叠部分
+    pub fn intersects(&self, r: &Rect) -> bool {
+        !(r.x >= (self.x + self.width as i32)
+            || self.x >= (r.x + r.width as i32)
+            || r.y >= (self.y + self.height as i32)
+            || self.y >= (r.y + r.height as i32))
+    }
+}

+ 6 - 0
starry_toolkit/src/lib.rs

@@ -0,0 +1,6 @@
+extern crate starry_client;
+extern crate starry_server;
+
+pub mod base;
+pub mod traits;
+pub mod widgets;

+ 33 - 0
starry_toolkit/src/main.rs

@@ -0,0 +1,33 @@
+use starry_server::core::{SCREEN_HEIGHT, SCREEN_WIDTH};
+use starry_toolkit::{
+    base::{panel::Panel, rect::Rect},
+    traits::{place::Place, text::Text},
+    widgets::{image::Image, label::Label},
+};
+
+const IMAGE_PATH: &[u8] = include_bytes!("./asset/desktop_bg.png");
+
+fn main() {
+    let panel = Panel::new(
+        Rect::new(0, 0, SCREEN_WIDTH as u32, SCREEN_HEIGHT as u32),
+        "Title",
+    );
+
+    // Label
+    let label = Label::new();
+    label.position(100, 100);
+    label.text("hello world");
+    label.text_offset(50, 50);
+    panel.add_child(&label);
+
+    // Image
+    let image = Image::from_path(IMAGE_PATH).unwrap();
+    image.position(0, SCREEN_HEIGHT as i32 / 2);
+    image.size(SCREEN_WIDTH as u32, SCREEN_HEIGHT as u32 / 2);
+    panel.add_child(&image);
+
+    panel.draw();
+
+    // 便于观察结果
+    loop {}
+}

+ 2 - 0
starry_toolkit/src/traits/mod.rs

@@ -0,0 +1,2 @@
+pub mod place;
+pub mod text;

+ 36 - 0
starry_toolkit/src/traits/place.rs

@@ -0,0 +1,36 @@
+use crate::widgets::{HorizontalPlacement, VerticalPlacement, Widget};
+
+pub trait Place: Sized + Widget {
+    fn position(&self, x: i32, y: i32) -> &Self {
+        let mut position = self.local_position().get();
+        let mut rect = self.rect().get();
+        position.x = x;
+        position.y = y;
+        rect.x = x;
+        rect.y = y;
+        self.local_position().set(position);
+        self.rect().set(rect);
+        self.arrange();
+        self
+    }
+
+    fn size(&self, width: u32, height: u32) -> &Self {
+        let mut rect = self.rect().get();
+        rect.width = width;
+        rect.height = height;
+        self.rect().set(rect);
+        self.arrange();
+        self
+    }
+
+    fn placement(
+        &self,
+        vertical_placement: VerticalPlacement,
+        horizontal_placement: HorizontalPlacement,
+    ) -> &Self {
+        self.vertical_placement().set(vertical_placement);
+        self.horizontal_placement().set(horizontal_placement);
+        self.arrange();
+        self
+    }
+}

+ 4 - 0
starry_toolkit/src/traits/text.rs

@@ -0,0 +1,4 @@
+pub trait Text {
+    fn text<S: Into<String>>(&self, text: S) -> &Self;
+    fn text_offset(&self, x: i32, y: i32) -> &Self;
+}

+ 85 - 0
starry_toolkit/src/widgets/image.rs

@@ -0,0 +1,85 @@
+use std::{
+    cell::{Cell, RefCell},
+    sync::Arc,
+};
+
+use starry_client::base::{color::Color, renderer::Renderer};
+
+use crate::{base::{point::Point, rect::Rect}, traits::place::Place};
+
+use super::{HorizontalPlacement, VerticalPlacement, Widget};
+
+use crate::starry_server::base::image::Image as ImageAsset;
+
+pub struct Image {
+    pub rect: Cell<Rect>,
+    local_position: Cell<Point>,
+    vertical_placement: Cell<VerticalPlacement>,
+    horizontal_placement: Cell<HorizontalPlacement>,
+    children: RefCell<Vec<Arc<dyn Widget>>>,
+    /// 图像源数据
+    pub image: RefCell<ImageAsset>,
+}
+
+impl Image {
+    pub fn new(width: u32, height: u32) -> Arc<Self> {
+        Self::from_image(ImageAsset::new(width as i32, height as i32))
+    }
+
+    pub fn from_color(width: u32, height: u32, color: Color) -> Arc<Self> {
+        Self::from_image(ImageAsset::from_color(width as i32, height as i32, color))
+    }
+
+    pub fn from_image(image: ImageAsset) -> Arc<Self> {
+        Arc::new(Image {
+            rect: Cell::new(Rect::new(0, 0, image.width() as u32, image.height() as u32)),
+            local_position: Cell::new(Point::new(0, 0)),
+            vertical_placement: Cell::new(VerticalPlacement::Absolute),
+            horizontal_placement: Cell::new(HorizontalPlacement::Absolute),
+            children: RefCell::new(vec![]),
+            image: RefCell::new(image),
+        })
+    }
+
+    pub fn from_path(path: &[u8]) -> Option<Arc<Self>> {
+        if let Some(image) = ImageAsset::from_path(path) {
+            Some(Self::from_image(image))
+        } else {
+            None
+        }
+    }
+}
+
+impl Place for Image {}
+
+impl Widget for Image {
+    fn name(&self) -> &str {
+        "Image"
+    }
+
+    fn rect(&self) -> &Cell<Rect> {
+        &self.rect
+    }
+
+    fn vertical_placement(&self) -> &Cell<VerticalPlacement> {
+        &self.vertical_placement
+    }
+
+    fn horizontal_placement(&self) -> &Cell<HorizontalPlacement> {
+        &self.horizontal_placement
+    }
+
+    fn local_position(&self) -> &Cell<Point> {
+        &self.local_position
+    }
+
+    fn children(&self) -> &RefCell<Vec<Arc<dyn Widget>>> {
+        &self.children
+    }
+
+    fn draw(&self, renderer: &mut dyn Renderer) {
+        let rect = self.rect.get();
+        let image = self.image.borrow();
+        renderer.image(rect.x, rect.y, rect.width, rect.height, image.data());
+    }
+}

+ 112 - 0
starry_toolkit/src/widgets/label.rs

@@ -0,0 +1,112 @@
+use std::{
+    cell::{Cell, RefCell},
+    sync::Arc,
+};
+
+use starry_client::base::{color::Color, renderer::Renderer};
+
+use crate::{base::{point::Point, rect::Rect}, traits::{place::Place, text::Text}};
+
+use super::{HorizontalPlacement, VerticalPlacement, Widget};
+
+pub struct Label {
+    pub rect: Cell<Rect>,
+    local_position: Cell<Point>,
+    vertical_placement: Cell<VerticalPlacement>,
+    horizontal_placement: Cell<HorizontalPlacement>,
+    children: RefCell<Vec<Arc<dyn Widget>>>,
+    pub text: RefCell<String>,
+    pub text_offset: Cell<Point>,
+}
+
+impl Label {
+        pub fn new() -> Arc<Self> {
+        Arc::new(Label {
+            rect: Cell::new(Rect::default()),
+            local_position: Cell::new(Point::new(0, 0)),
+            vertical_placement: Cell::new(VerticalPlacement::Absolute),
+            horizontal_placement: Cell::new(HorizontalPlacement::Absolute),
+            children: RefCell::new(vec![]),
+            text: RefCell::new(String::new()),
+            text_offset: Cell::new(Point::default()),
+        })
+    }
+
+    fn adjust_size(&self) {
+        let text = self.text.borrow();
+        self.size(
+            text.len() as u32 * 8 + 2 * self.text_offset.get().x as u32,
+            16 + 2 * self.text_offset.get().y as u32,
+        );
+    }
+}
+
+impl Place for Label {}
+
+impl Widget for Label {
+    fn name(&self) -> &str {
+        "Label"
+    }
+
+    fn rect(&self) -> &Cell<Rect> {
+        &self.rect
+    }
+
+    fn local_position(&self) -> &Cell<Point> {
+        &self.local_position
+    }
+
+    fn vertical_placement(&self) -> &Cell<VerticalPlacement> {
+        &self.vertical_placement
+    }
+
+    fn horizontal_placement(&self) -> &Cell<HorizontalPlacement> {
+        &self.horizontal_placement
+    }
+
+    fn children(&self) -> &RefCell<Vec<Arc<dyn Widget>>> {
+        &self.children
+    }
+
+    fn draw(&self, renderer: &mut dyn Renderer) {
+        let origin_rect = self.rect().get();
+        let mut current_rect = self.rect().get(); // 当前字符渲染矩形
+        let origin_x = origin_rect.x;
+        let text = self.text.borrow().clone();
+
+        for char in text.chars() {
+            if char == '\n' {
+                // 换行 退格到起始位置
+                current_rect.x = origin_x;
+                current_rect.y += 16;
+            } else {
+                // 避免超出矩形范围
+                if current_rect.x + 8 <= origin_rect.x + origin_rect.width as i32
+                    && current_rect.y + 16 <= origin_rect.y + origin_rect.height as i32
+                {
+                    // 默认渲染白色字体
+                    // TODO 应用主题(Theme)颜色
+                    renderer.char(current_rect.x, current_rect.y, char, Color::rgb(255, 255, 255));
+                }
+                current_rect.x += 8;
+            }
+        }
+    }
+}
+
+impl Text for Label {
+    fn text<S: Into<String>>(&self, target_text: S) -> &Self {
+        {
+            let mut text = self.text.borrow_mut();
+            *text = target_text.into();
+        }
+        self.adjust_size();
+        self
+    }
+
+    fn text_offset(&self, x: i32, y: i32) -> &Self {
+        self.text_offset.set(Point::new(x, y));
+        self.adjust_size();
+        self
+    }
+}

+ 78 - 0
starry_toolkit/src/widgets/mod.rs

@@ -0,0 +1,78 @@
+use std::{
+    any::Any,
+    cell::{Cell, RefCell},
+    sync::Arc,
+};
+
+use starry_client::base::renderer::Renderer;
+
+use crate::base::{point::Point, rect::Rect};
+
+pub mod image;
+pub mod label;
+
+/// 组件的纵向排列方式
+#[derive(PartialEq, Copy, Clone)]
+pub enum VerticalPlacement {
+    /// 向上对齐
+    Top,
+    /// 居中对齐
+    Center,
+    /// 向下对齐
+    Bottom,
+    /// 绝对位置
+    Absolute,
+    /// 拉伸
+    Stretch,
+}
+
+/// 组件的横向排列方式
+#[derive(PartialEq, Copy, Clone)]
+pub enum HorizontalPlacement {
+    /// 靠左对齐
+    Left,
+    /// 居中对齐
+    Center,
+    /// 靠右对齐
+    Right,
+    /// 绝对位置
+    Absolute,
+    /// 拉伸
+    Stretch,
+}
+
+///  UI组件需要实现的特性
+pub trait Widget: Any {
+    /// 返回渲染的矩形区域
+    fn rect(&self) -> &Cell<Rect>;
+
+    /// 返回组件相对于父物体的相对位置
+    fn local_position(&self) -> &Cell<Point>;
+
+    /// 返回纵向排列方式
+    fn vertical_placement(&self) -> &Cell<VerticalPlacement>;
+
+    /// 返回横向排列方式
+    fn horizontal_placement(&self) -> &Cell<HorizontalPlacement>;
+
+    /// 返回组件的名字
+    fn name(&self) -> &str;
+
+    /// 返回子组件数组
+    fn children(&self) -> &RefCell<Vec<Arc<dyn Widget>>>;
+
+    /// 添加子组件
+    fn add_child(&self, widget: Arc<dyn Widget>) {
+        (*self.children().borrow_mut()).push(widget);
+        self.arrange();
+    }
+
+    /// 渲染组件
+    fn draw(&self, renderer: &mut dyn Renderer);
+
+    /// 更新组件状态
+    fn update(&self) {}
+
+    /// TODO
+    fn arrange(&self) {}
+}