Merge pull request #2698 from iced-rs/feature/test-crate
Headless Mode Testing
This commit is contained in:
commit
f2c9b6b2ff
44 changed files with 8395 additions and 196 deletions
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
|
@ -8,7 +8,7 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
rust: [stable, beta, "1.80"]
|
||||
rust: [stable, beta, "1.81"]
|
||||
steps:
|
||||
- uses: hecrj/setup-rust-action@v2
|
||||
with:
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,6 +1,5 @@
|
|||
target/
|
||||
pkg/
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
dist/
|
||||
traces/
|
||||
|
|
|
|||
7150
Cargo.lock
generated
Normal file
7150
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
12
Cargo.toml
12
Cargo.toml
|
|
@ -22,7 +22,7 @@ all-features = true
|
|||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[features]
|
||||
default = ["wgpu", "tiny-skia", "fira-sans", "auto-detect-theme"]
|
||||
default = ["wgpu", "tiny-skia", "auto-detect-theme"]
|
||||
# Enables the `wgpu` GPU-accelerated renderer backend
|
||||
wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"]
|
||||
# Enables the `tiny-skia` software renderer backend
|
||||
|
|
@ -53,13 +53,13 @@ smol = ["iced_futures/smol"]
|
|||
system = ["iced_winit/system"]
|
||||
# Enables broken "sRGB linear" blending to reproduce color management of the Web
|
||||
web-colors = ["iced_renderer/web-colors"]
|
||||
# Enables the WebGL backend, replacing WebGPU
|
||||
# Enables the WebGL backend
|
||||
webgl = ["iced_renderer/webgl"]
|
||||
# Enables syntax highligthing
|
||||
highlighter = ["iced_highlighter", "iced_widget/highlighter"]
|
||||
# Enables the advanced module
|
||||
advanced = ["iced_core/advanced", "iced_widget/advanced"]
|
||||
# Embeds Fira Sans as the default font on Wasm builds
|
||||
# Embeds Fira Sans into the final application; useful for testing and Wasm builds
|
||||
fira-sans = ["iced_renderer/fira-sans"]
|
||||
# Auto-detects light/dark mode for the built-in theme
|
||||
auto-detect-theme = ["iced_core/auto-detect-theme"]
|
||||
|
|
@ -111,6 +111,7 @@ members = [
|
|||
"highlighter",
|
||||
"renderer",
|
||||
"runtime",
|
||||
"test",
|
||||
"tiny_skia",
|
||||
"wgpu",
|
||||
"widget",
|
||||
|
|
@ -127,7 +128,7 @@ repository = "https://github.com/iced-rs/iced"
|
|||
homepage = "https://iced.rs"
|
||||
categories = ["gui"]
|
||||
keywords = ["gui", "ui", "graphics", "interface", "widgets"]
|
||||
rust-version = "1.80"
|
||||
rust-version = "1.81"
|
||||
|
||||
[workspace.dependencies]
|
||||
iced = { version = "0.14.0-dev", path = "." }
|
||||
|
|
@ -137,6 +138,7 @@ iced_graphics = { version = "0.14.0-dev", path = "graphics" }
|
|||
iced_highlighter = { version = "0.14.0-dev", path = "highlighter" }
|
||||
iced_renderer = { version = "0.14.0-dev", path = "renderer" }
|
||||
iced_runtime = { version = "0.14.0-dev", path = "runtime" }
|
||||
iced_test = { version = "0.14.0-dev", path = "test" }
|
||||
iced_tiny_skia = { version = "0.14.0-dev", path = "tiny_skia" }
|
||||
iced_wgpu = { version = "0.14.0-dev", path = "wgpu" }
|
||||
iced_widget = { version = "0.14.0-dev", path = "widget" }
|
||||
|
|
@ -163,11 +165,13 @@ num-traits = "0.2"
|
|||
once_cell = "1.0"
|
||||
ouroboros = "0.18"
|
||||
palette = "0.7"
|
||||
png = "0.17"
|
||||
pulldown-cmark = "0.11"
|
||||
qrcode = { version = "0.13", default-features = false }
|
||||
raw-window-handle = "0.6"
|
||||
resvg = "0.42"
|
||||
rustc-hash = "2.0"
|
||||
sha2 = "0.10"
|
||||
smol = "1.0"
|
||||
smol_str = "0.2"
|
||||
softbuffer = "0.4"
|
||||
|
|
|
|||
|
|
@ -32,6 +32,12 @@ impl Key {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Named> for Key {
|
||||
fn from(named: Named) -> Self {
|
||||
Self::Named(named)
|
||||
}
|
||||
}
|
||||
|
||||
/// A named key.
|
||||
///
|
||||
/// This is mostly the `NamedKey` type found in [`winit`].
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ mod pixels;
|
|||
mod point;
|
||||
mod rectangle;
|
||||
mod rotation;
|
||||
mod settings;
|
||||
mod shadow;
|
||||
mod shell;
|
||||
mod size;
|
||||
|
|
@ -67,6 +68,7 @@ pub use point::Point;
|
|||
pub use rectangle::Rectangle;
|
||||
pub use renderer::Renderer;
|
||||
pub use rotation::Rotation;
|
||||
pub use settings::Settings;
|
||||
pub use shadow::Shadow;
|
||||
pub use shell::Shell;
|
||||
pub use size::Size;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
mod null;
|
||||
|
||||
use crate::{
|
||||
Background, Border, Color, Rectangle, Shadow, Size, Transformation, Vector,
|
||||
Background, Border, Color, Font, Pixels, Rectangle, Shadow, Size,
|
||||
Transformation, Vector,
|
||||
};
|
||||
|
||||
/// A component that can be used by widgets to draw themselves on a screen.
|
||||
|
|
@ -100,3 +101,19 @@ impl Default for Style {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A headless renderer is a renderer that can render offscreen without
|
||||
/// a window nor a compositor.
|
||||
pub trait Headless {
|
||||
/// Creates a new [`Headless`] renderer;
|
||||
fn new(default_font: Font, default_text_size: Pixels) -> Self;
|
||||
|
||||
/// Draws offscreen into a screenshot, returning a collection of
|
||||
/// bytes representing the rendered pixels in RGBA order.
|
||||
fn screenshot(
|
||||
&mut self,
|
||||
size: Size<u32>,
|
||||
scale_factor: f32,
|
||||
background_color: Color,
|
||||
) -> Vec<u8>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,11 +29,9 @@ pub struct Settings {
|
|||
/// primitives.
|
||||
///
|
||||
/// Enabling it can produce a smoother result in some widgets, like the
|
||||
/// [`Canvas`], at a performance cost.
|
||||
/// `canvas` widget, at a performance cost.
|
||||
///
|
||||
/// By default, it is disabled.
|
||||
///
|
||||
/// [`Canvas`]: crate::widget::Canvas
|
||||
pub antialiasing: bool,
|
||||
}
|
||||
|
||||
|
|
@ -48,12 +46,3 @@ impl Default for Settings {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Settings> for iced_winit::Settings {
|
||||
fn from(settings: Settings) -> iced_winit::Settings {
|
||||
iced_winit::Settings {
|
||||
id: settings.id,
|
||||
fonts: settings.fonts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@ pub mod palette;
|
|||
|
||||
pub use palette::Palette;
|
||||
|
||||
use crate::Color;
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
|
@ -246,3 +248,35 @@ impl fmt::Display for Custom {
|
|||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
/// The base style of a [`Theme`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Style {
|
||||
/// The background [`Color`] of the application.
|
||||
pub background_color: Color,
|
||||
|
||||
/// The default text [`Color`] of the application.
|
||||
pub text_color: Color,
|
||||
}
|
||||
|
||||
/// The default blank style of a [`Theme`].
|
||||
pub trait Base {
|
||||
/// Returns the default base [`Style`] of a [`Theme`].
|
||||
fn base(&self) -> Style;
|
||||
}
|
||||
|
||||
impl Base for Theme {
|
||||
fn base(&self) -> Style {
|
||||
default(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// The default [`Style`] of a built-in [`Theme`].
|
||||
pub fn default(theme: &Theme) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
Style {
|
||||
background_color: palette.background.base.color,
|
||||
text_color: palette.background.base.text,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,24 +30,45 @@ pub trait Operation<T = ()>: Send {
|
|||
);
|
||||
|
||||
/// Operates on a widget that can be focused.
|
||||
fn focusable(&mut self, _state: &mut dyn Focusable, _id: Option<&Id>) {}
|
||||
fn focusable(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
_state: &mut dyn Focusable,
|
||||
) {
|
||||
}
|
||||
|
||||
/// Operates on a widget that can be scrolled.
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
_state: &mut dyn Scrollable,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
_content_bounds: Rectangle,
|
||||
_translation: Vector,
|
||||
_state: &mut dyn Scrollable,
|
||||
) {
|
||||
}
|
||||
|
||||
/// Operates on a widget that has text input.
|
||||
fn text_input(&mut self, _state: &mut dyn TextInput, _id: Option<&Id>) {}
|
||||
fn text_input(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
_state: &mut dyn TextInput,
|
||||
) {
|
||||
}
|
||||
|
||||
/// Operates on a widget that contains some text.
|
||||
fn text(&mut self, _id: Option<&Id>, _bounds: Rectangle, _text: &str) {}
|
||||
|
||||
/// Operates on a custom widget with some state.
|
||||
fn custom(&mut self, _state: &mut dyn Any, _id: Option<&Id>) {}
|
||||
fn custom(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
_state: &mut dyn Any,
|
||||
) {
|
||||
}
|
||||
|
||||
/// Finishes the [`Operation`] and returns its [`Outcome`].
|
||||
fn finish(&self) -> Outcome<T> {
|
||||
|
|
@ -68,33 +89,52 @@ where
|
|||
self.as_mut().container(id, bounds, operate_on_children);
|
||||
}
|
||||
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
|
||||
self.as_mut().focusable(state, id);
|
||||
fn focusable(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
self.as_mut().focusable(id, bounds, state);
|
||||
}
|
||||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn Scrollable,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
content_bounds: Rectangle,
|
||||
translation: Vector,
|
||||
state: &mut dyn Scrollable,
|
||||
) {
|
||||
self.as_mut().scrollable(
|
||||
state,
|
||||
id,
|
||||
bounds,
|
||||
content_bounds,
|
||||
translation,
|
||||
state,
|
||||
);
|
||||
}
|
||||
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
self.as_mut().text_input(state, id);
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
self.as_mut().text_input(id, bounds, state);
|
||||
}
|
||||
|
||||
fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) {
|
||||
self.as_mut().custom(state, id);
|
||||
fn text(&mut self, id: Option<&Id>, bounds: Rectangle, text: &str) {
|
||||
self.as_mut().text(id, bounds, text);
|
||||
}
|
||||
|
||||
fn custom(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Any,
|
||||
) {
|
||||
self.as_mut().custom(id, bounds, state);
|
||||
}
|
||||
|
||||
fn finish(&self) -> Outcome<O> {
|
||||
|
|
@ -150,33 +190,52 @@ where
|
|||
});
|
||||
}
|
||||
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
|
||||
self.operation.focusable(state, id);
|
||||
fn focusable(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
self.operation.focusable(id, bounds, state);
|
||||
}
|
||||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn Scrollable,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
content_bounds: Rectangle,
|
||||
translation: Vector,
|
||||
state: &mut dyn Scrollable,
|
||||
) {
|
||||
self.operation.scrollable(
|
||||
state,
|
||||
id,
|
||||
bounds,
|
||||
content_bounds,
|
||||
translation,
|
||||
state,
|
||||
);
|
||||
}
|
||||
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
self.operation.text_input(state, id);
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
self.operation.text_input(id, bounds, state);
|
||||
}
|
||||
|
||||
fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) {
|
||||
self.operation.custom(state, id);
|
||||
fn text(&mut self, id: Option<&Id>, bounds: Rectangle, text: &str) {
|
||||
self.operation.text(id, bounds, text);
|
||||
}
|
||||
|
||||
fn custom(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Any,
|
||||
) {
|
||||
self.operation.custom(id, bounds, state);
|
||||
}
|
||||
|
||||
fn finish(&self) -> Outcome<O> {
|
||||
|
|
@ -234,39 +293,55 @@ where
|
|||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn Scrollable,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
content_bounds: Rectangle,
|
||||
translation: Vector,
|
||||
state: &mut dyn Scrollable,
|
||||
) {
|
||||
self.operation.scrollable(
|
||||
state,
|
||||
id,
|
||||
bounds,
|
||||
content_bounds,
|
||||
translation,
|
||||
state,
|
||||
);
|
||||
}
|
||||
|
||||
fn focusable(
|
||||
&mut self,
|
||||
state: &mut dyn Focusable,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
self.operation.focusable(state, id);
|
||||
self.operation.focusable(id, bounds, state);
|
||||
}
|
||||
|
||||
fn text_input(
|
||||
&mut self,
|
||||
state: &mut dyn TextInput,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
self.operation.text_input(state, id);
|
||||
self.operation.text_input(id, bounds, state);
|
||||
}
|
||||
|
||||
fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) {
|
||||
self.operation.custom(state, id);
|
||||
fn text(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
text: &str,
|
||||
) {
|
||||
self.operation.text(id, bounds, text);
|
||||
}
|
||||
|
||||
fn custom(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Any,
|
||||
) {
|
||||
self.operation.custom(id, bounds, state);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -275,33 +350,52 @@ where
|
|||
MapRef { operation }.container(id, bounds, operate_on_children);
|
||||
}
|
||||
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
|
||||
self.operation.focusable(state, id);
|
||||
fn focusable(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
self.operation.focusable(id, bounds, state);
|
||||
}
|
||||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn Scrollable,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
content_bounds: Rectangle,
|
||||
translation: Vector,
|
||||
state: &mut dyn Scrollable,
|
||||
) {
|
||||
self.operation.scrollable(
|
||||
state,
|
||||
id,
|
||||
bounds,
|
||||
content_bounds,
|
||||
translation,
|
||||
state,
|
||||
);
|
||||
}
|
||||
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
self.operation.text_input(state, id);
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
self.operation.text_input(id, bounds, state);
|
||||
}
|
||||
|
||||
fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) {
|
||||
self.operation.custom(state, id);
|
||||
fn text(&mut self, id: Option<&Id>, bounds: Rectangle, text: &str) {
|
||||
self.operation.text(id, bounds, text);
|
||||
}
|
||||
|
||||
fn custom(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Any,
|
||||
) {
|
||||
self.operation.custom(id, bounds, state);
|
||||
}
|
||||
|
||||
fn finish(&self) -> Outcome<B> {
|
||||
|
|
@ -361,33 +455,52 @@ where
|
|||
});
|
||||
}
|
||||
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
|
||||
self.operation.focusable(state, id);
|
||||
fn focusable(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
self.operation.focusable(id, bounds, state);
|
||||
}
|
||||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn Scrollable,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
content_bounds: Rectangle,
|
||||
translation: crate::Vector,
|
||||
state: &mut dyn Scrollable,
|
||||
) {
|
||||
self.operation.scrollable(
|
||||
state,
|
||||
id,
|
||||
bounds,
|
||||
content_bounds,
|
||||
translation,
|
||||
state,
|
||||
);
|
||||
}
|
||||
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
self.operation.text_input(state, id);
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
self.operation.text_input(id, bounds, state);
|
||||
}
|
||||
|
||||
fn custom(&mut self, state: &mut dyn std::any::Any, id: Option<&Id>) {
|
||||
self.operation.custom(state, id);
|
||||
fn text(&mut self, id: Option<&Id>, bounds: Rectangle, text: &str) {
|
||||
self.operation.text(id, bounds, text);
|
||||
}
|
||||
|
||||
fn custom(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Any,
|
||||
) {
|
||||
self.operation.custom(id, bounds, state);
|
||||
}
|
||||
|
||||
fn finish(&self) -> Outcome<B> {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,12 @@ pub fn focus<T>(target: Id) -> impl Operation<T> {
|
|||
}
|
||||
|
||||
impl<T> Operation<T> for Focus {
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
|
||||
fn focusable(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
match id {
|
||||
Some(id) if id == &self.target => {
|
||||
state.focus();
|
||||
|
|
@ -64,7 +69,12 @@ pub fn count() -> impl Operation<Count> {
|
|||
}
|
||||
|
||||
impl Operation<Count> for CountFocusable {
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) {
|
||||
fn focusable(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
if state.is_focused() {
|
||||
self.count.focused = Some(self.count.total);
|
||||
}
|
||||
|
|
@ -104,7 +114,12 @@ where
|
|||
}
|
||||
|
||||
impl<T> Operation<T> for FocusPrevious {
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) {
|
||||
fn focusable(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
if self.count.total == 0 {
|
||||
return;
|
||||
}
|
||||
|
|
@ -147,7 +162,12 @@ where
|
|||
}
|
||||
|
||||
impl<T> Operation<T> for FocusNext {
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) {
|
||||
fn focusable(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
match self.count.focused {
|
||||
None if self.current == 0 => state.focus(),
|
||||
Some(focused) if focused == self.current => state.unfocus(),
|
||||
|
|
@ -179,7 +199,12 @@ pub fn find_focused() -> impl Operation<Id> {
|
|||
}
|
||||
|
||||
impl Operation<Id> for FindFocused {
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
|
||||
fn focusable(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
if state.is_focused() && id.is_some() {
|
||||
self.focused = id.cloned();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,11 +39,11 @@ pub fn snap_to<T>(target: Id, offset: RelativeOffset) -> impl Operation<T> {
|
|||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn Scrollable,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
_content_bounds: Rectangle,
|
||||
_translation: Vector,
|
||||
state: &mut dyn Scrollable,
|
||||
) {
|
||||
if Some(&self.target) == id {
|
||||
state.snap_to(self.offset);
|
||||
|
|
@ -74,11 +74,11 @@ pub fn scroll_to<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
|
|||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn Scrollable,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
_content_bounds: Rectangle,
|
||||
_translation: Vector,
|
||||
state: &mut dyn Scrollable,
|
||||
) {
|
||||
if Some(&self.target) == id {
|
||||
state.scroll_to(self.offset);
|
||||
|
|
@ -109,11 +109,11 @@ pub fn scroll_by<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
|
|||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn Scrollable,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
content_bounds: Rectangle,
|
||||
_translation: Vector,
|
||||
state: &mut dyn Scrollable,
|
||||
) {
|
||||
if Some(&self.target) == id {
|
||||
state.scroll_by(self.offset, bounds, content_bounds);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,12 @@ pub fn move_cursor_to_front<T>(target: Id) -> impl Operation<T> {
|
|||
}
|
||||
|
||||
impl<T> Operation<T> for MoveCursor {
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
match id {
|
||||
Some(id) if id == &self.target => {
|
||||
state.move_cursor_to_front();
|
||||
|
|
@ -53,7 +58,12 @@ pub fn move_cursor_to_end<T>(target: Id) -> impl Operation<T> {
|
|||
}
|
||||
|
||||
impl<T> Operation<T> for MoveCursor {
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
match id {
|
||||
Some(id) if id == &self.target => {
|
||||
state.move_cursor_to_end();
|
||||
|
|
@ -84,7 +94,12 @@ pub fn move_cursor_to<T>(target: Id, position: usize) -> impl Operation<T> {
|
|||
}
|
||||
|
||||
impl<T> Operation<T> for MoveCursor {
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
match id {
|
||||
Some(id) if id == &self.target => {
|
||||
state.move_cursor_to(self.position);
|
||||
|
|
@ -113,7 +128,12 @@ pub fn select_all<T>(target: Id) -> impl Operation<T> {
|
|||
}
|
||||
|
||||
impl<T> Operation<T> for MoveCursor {
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
match id {
|
||||
Some(id) if id == &self.target => {
|
||||
state.select_all();
|
||||
|
|
|
|||
|
|
@ -267,6 +267,16 @@ where
|
|||
|
||||
draw(renderer, defaults, layout, state.0.raw(), style, viewport);
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
_state: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
_renderer: &Renderer,
|
||||
operation: &mut dyn super::Operation,
|
||||
) {
|
||||
operation.text(None, layout.bounds(), &self.fragment);
|
||||
}
|
||||
}
|
||||
|
||||
/// Produces the [`layout::Node`] of a [`Text`] widget.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
//! Build window-based GUI applications.
|
||||
pub mod icon;
|
||||
pub mod screenshot;
|
||||
pub mod settings;
|
||||
|
||||
mod event;
|
||||
|
|
@ -17,5 +18,6 @@ pub use level::Level;
|
|||
pub use mode::Mode;
|
||||
pub use position::Position;
|
||||
pub use redraw_request::RedrawRequest;
|
||||
pub use screenshot::Screenshot;
|
||||
pub use settings::Settings;
|
||||
pub use user_attention::UserAttention;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//! Take screenshots of a window.
|
||||
use crate::core::{Rectangle, Size};
|
||||
use crate::{Rectangle, Size};
|
||||
|
||||
use bytes::Bytes;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
|
|
@ -28,6 +28,7 @@ use crate::window::{Icon, Level, Position};
|
|||
use crate::Size;
|
||||
|
||||
pub use platform::PlatformSpecific;
|
||||
|
||||
/// The window settings of an application.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Settings {
|
||||
|
|
|
|||
|
|
@ -10,4 +10,7 @@ iced.workspace = true
|
|||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
iced.workspace = true
|
||||
iced.features = ["webgl"]
|
||||
iced.features = ["webgl", "fira-sans"]
|
||||
|
||||
[dev-dependencies]
|
||||
iced_test.workspace = true
|
||||
|
|
|
|||
|
|
@ -38,3 +38,31 @@ impl Counter {
|
|||
.align_x(Center)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use iced_test::selector::text;
|
||||
use iced_test::{simulator, Error};
|
||||
|
||||
#[test]
|
||||
fn it_counts() -> Result<(), Error> {
|
||||
let mut counter = Counter { value: 0 };
|
||||
let mut ui = simulator(counter.view());
|
||||
|
||||
let _ = ui.click(text("Increment"))?;
|
||||
let _ = ui.click(text("Increment"))?;
|
||||
let _ = ui.click(text("Decrement"))?;
|
||||
|
||||
for message in ui.into_messages() {
|
||||
counter.update(message);
|
||||
}
|
||||
|
||||
assert_eq!(counter.value, 1);
|
||||
|
||||
let mut ui = simulator(counter.view());
|
||||
assert!(ui.find(text("1")).is_ok(), "Counter should display 1!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use iced::application;
|
||||
use iced::gradient;
|
||||
use iced::theme;
|
||||
use iced::widget::{
|
||||
checkbox, column, container, horizontal_space, row, slider, text,
|
||||
};
|
||||
|
|
@ -95,16 +95,14 @@ impl Gradient {
|
|||
.into()
|
||||
}
|
||||
|
||||
fn style(&self, theme: &Theme) -> application::Appearance {
|
||||
use application::DefaultStyle;
|
||||
|
||||
fn style(&self, theme: &Theme) -> theme::Style {
|
||||
if self.transparent {
|
||||
application::Appearance {
|
||||
theme::Style {
|
||||
background_color: Color::TRANSPARENT,
|
||||
text_color: theme.palette().text,
|
||||
}
|
||||
} else {
|
||||
Theme::default_style(theme)
|
||||
theme::default(theme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,12 +20,15 @@ tracing-subscriber = "0.3"
|
|||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
iced.workspace = true
|
||||
iced.features = ["debug", "webgl"]
|
||||
iced.features = ["debug", "webgl", "fira-sans"]
|
||||
|
||||
uuid = { version = "1.0", features = ["js"] }
|
||||
web-sys = { workspace = true, features = ["Window", "Storage"] }
|
||||
wasm-timer.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
iced_test.workspace = true
|
||||
|
||||
[package.metadata.deb]
|
||||
assets = [
|
||||
["target/release-opt/todos", "usr/bin/iced-todos", "755"],
|
||||
|
|
|
|||
1
examples/todos/snapshots/creates_a_new_task.sha256
Normal file
1
examples/todos/snapshots/creates_a_new_task.sha256
Normal file
|
|
@ -0,0 +1 @@
|
|||
3160686067cb7c738802009cdf2f3c5f5a5bd8c89ada70517388b7adbe64c313
|
||||
|
|
@ -15,7 +15,7 @@ pub fn main() -> iced::Result {
|
|||
|
||||
iced::application(Todos::title, Todos::update, Todos::view)
|
||||
.subscription(Todos::subscription)
|
||||
.font(include_bytes!("../fonts/icons.ttf").as_slice())
|
||||
.font(Todos::ICON_FONT)
|
||||
.window_size((500.0, 800.0))
|
||||
.run_with(Todos::new)
|
||||
}
|
||||
|
|
@ -48,6 +48,8 @@ enum Message {
|
|||
}
|
||||
|
||||
impl Todos {
|
||||
const ICON_FONT: &'static [u8] = include_bytes!("../fonts/icons.ttf");
|
||||
|
||||
fn new() -> (Self, Command<Message>) {
|
||||
(
|
||||
Self::Loading,
|
||||
|
|
@ -449,11 +451,10 @@ fn empty_message(message: &str) -> Element<'_, Message> {
|
|||
}
|
||||
|
||||
// Fonts
|
||||
const ICONS: Font = Font::with_name("Iced-Todos-Icons");
|
||||
|
||||
fn icon(unicode: char) -> Text<'static> {
|
||||
text(unicode.to_string())
|
||||
.font(ICONS)
|
||||
.font(Font::with_name("Iced-Todos-Icons"))
|
||||
.width(20)
|
||||
.align_x(Center)
|
||||
}
|
||||
|
|
@ -584,3 +585,49 @@ impl SavedState {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use iced::{Settings, Theme};
|
||||
use iced_test::selector::text;
|
||||
use iced_test::{Error, Simulator};
|
||||
|
||||
fn simulator(todos: &Todos) -> Simulator<Message> {
|
||||
Simulator::with_settings(
|
||||
Settings {
|
||||
fonts: vec![Todos::ICON_FONT.into()],
|
||||
..Settings::default()
|
||||
},
|
||||
todos.view(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_creates_a_new_task() -> Result<(), Error> {
|
||||
let (mut todos, _command) = Todos::new();
|
||||
let _command = todos.update(Message::Loaded(Err(LoadError::File)));
|
||||
|
||||
let mut ui = simulator(&todos);
|
||||
let _input = ui.click("new-task")?;
|
||||
|
||||
let _ = ui.typewrite("Create the universe");
|
||||
let _ = ui.tap_key(keyboard::key::Named::Enter);
|
||||
|
||||
for message in ui.into_messages() {
|
||||
let _command = todos.update(message);
|
||||
}
|
||||
|
||||
let mut ui = simulator(&todos);
|
||||
let _ = ui.find(text("Create the universe"))?;
|
||||
|
||||
let snapshot = ui.snapshot(&Theme::Dark)?;
|
||||
assert!(
|
||||
snapshot.matches_hash("snapshots/creates_a_new_task")?,
|
||||
"snapshots should match!"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ tracing-subscriber = "0.3"
|
|||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
iced.workspace = true
|
||||
iced.features = ["image", "debug", "webgl"]
|
||||
iced.features = ["image", "debug", "webgl", "fira-sans"]
|
||||
|
||||
console_error_panic_hook = "0.1"
|
||||
console_log = "1.0"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ use crate::core::{Color, Pixels, Point, Rectangle, Size, Transformation};
|
|||
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Arc, RwLock, Weak};
|
||||
|
||||
/// A text primitive.
|
||||
|
|
@ -146,11 +147,12 @@ impl Text {
|
|||
|
||||
/// The regular variant of the [Fira Sans] font.
|
||||
///
|
||||
/// It is loaded as part of the default fonts in Wasm builds.
|
||||
/// It is loaded as part of the default fonts when the `fira-sans`
|
||||
/// feature is enabled.
|
||||
///
|
||||
/// [Fira Sans]: https://mozilla.github.io/Fira/
|
||||
#[cfg(all(target_arch = "wasm32", feature = "fira-sans"))]
|
||||
pub const FIRA_SANS_REGULAR: &'static [u8] =
|
||||
#[cfg(feature = "fira-sans")]
|
||||
pub const FIRA_SANS_REGULAR: &[u8] =
|
||||
include_bytes!("../fonts/FiraSans-Regular.ttf").as_slice();
|
||||
|
||||
/// Returns the global [`FontSystem`].
|
||||
|
|
@ -163,11 +165,12 @@ pub fn font_system() -> &'static RwLock<FontSystem> {
|
|||
cosmic_text::fontdb::Source::Binary(Arc::new(
|
||||
include_bytes!("../fonts/Iced-Icons.ttf").as_slice(),
|
||||
)),
|
||||
#[cfg(all(target_arch = "wasm32", feature = "fira-sans"))]
|
||||
#[cfg(feature = "fira-sans")]
|
||||
cosmic_text::fontdb::Source::Binary(Arc::new(
|
||||
include_bytes!("../fonts/FiraSans-Regular.ttf").as_slice(),
|
||||
)),
|
||||
]),
|
||||
loaded_fonts: HashSet::new(),
|
||||
version: Version::default(),
|
||||
})
|
||||
})
|
||||
|
|
@ -177,6 +180,7 @@ pub fn font_system() -> &'static RwLock<FontSystem> {
|
|||
#[allow(missing_debug_implementations)]
|
||||
pub struct FontSystem {
|
||||
raw: cosmic_text::FontSystem,
|
||||
loaded_fonts: HashSet<usize>,
|
||||
version: Version,
|
||||
}
|
||||
|
||||
|
|
@ -188,6 +192,14 @@ impl FontSystem {
|
|||
|
||||
/// Loads a font from its bytes.
|
||||
pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
|
||||
if let Cow::Borrowed(bytes) = bytes {
|
||||
let address = bytes.as_ptr() as usize;
|
||||
|
||||
if !self.loaded_fonts.insert(address) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self.raw.db_mut().load_font_source(
|
||||
cosmic_text::fontdb::Source::Binary(Arc::new(bytes.into_owned())),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ pub type Compositor = renderer::Compositor;
|
|||
|
||||
#[cfg(all(feature = "wgpu", feature = "tiny-skia"))]
|
||||
mod renderer {
|
||||
use crate::core::renderer;
|
||||
use crate::core::{Color, Font, Pixels, Size};
|
||||
|
||||
pub type Renderer = crate::fallback::Renderer<
|
||||
iced_wgpu::Renderer,
|
||||
iced_tiny_skia::Renderer,
|
||||
|
|
@ -32,6 +35,31 @@ mod renderer {
|
|||
iced_wgpu::window::Compositor,
|
||||
iced_tiny_skia::window::Compositor,
|
||||
>;
|
||||
|
||||
impl renderer::Headless for Renderer {
|
||||
fn new(default_font: Font, default_text_size: Pixels) -> Self {
|
||||
Self::Secondary(iced_tiny_skia::Renderer::new(
|
||||
default_font,
|
||||
default_text_size,
|
||||
))
|
||||
}
|
||||
|
||||
fn screenshot(
|
||||
&mut self,
|
||||
size: Size<u32>,
|
||||
scale_factor: f32,
|
||||
background_color: Color,
|
||||
) -> Vec<u8> {
|
||||
match self {
|
||||
crate::fallback::Renderer::Primary(_) => unreachable!(
|
||||
"iced_wgpu does not support headless mode yet!"
|
||||
),
|
||||
crate::fallback::Renderer::Secondary(renderer) => {
|
||||
renderer.screenshot(size, scale_factor, background_color)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "wgpu", not(feature = "tiny-skia")))]
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
//! Build window-based GUI applications.
|
||||
pub mod screenshot;
|
||||
|
||||
pub use screenshot::Screenshot;
|
||||
|
||||
use crate::core::time::Instant;
|
||||
use crate::core::window::{
|
||||
Event, Icon, Id, Level, Mode, Settings, UserAttention,
|
||||
Event, Icon, Id, Level, Mode, Screenshot, Settings, UserAttention,
|
||||
};
|
||||
use crate::core::{Point, Size};
|
||||
use crate::futures::event;
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
//! }
|
||||
//! ```
|
||||
use crate::program::{self, Program};
|
||||
use crate::theme;
|
||||
use crate::window;
|
||||
use crate::{
|
||||
Element, Executor, Font, Result, Settings, Size, Subscription, Task,
|
||||
|
|
@ -38,8 +39,6 @@ use crate::{
|
|||
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub use crate::shell::program::{Appearance, DefaultStyle};
|
||||
|
||||
/// Creates an iced [`Application`] given its title, update, and view logic.
|
||||
///
|
||||
/// # Example
|
||||
|
|
@ -76,7 +75,7 @@ pub fn application<State, Message, Theme, Renderer>(
|
|||
where
|
||||
State: 'static,
|
||||
Message: Send + std::fmt::Debug + 'static,
|
||||
Theme: Default + DefaultStyle,
|
||||
Theme: Default + theme::Base,
|
||||
Renderer: program::Renderer,
|
||||
{
|
||||
use std::marker::PhantomData;
|
||||
|
|
@ -94,7 +93,7 @@ where
|
|||
for Instance<State, Message, Theme, Renderer, Update, View>
|
||||
where
|
||||
Message: Send + std::fmt::Debug + 'static,
|
||||
Theme: Default + DefaultStyle,
|
||||
Theme: Default + theme::Base,
|
||||
Renderer: program::Renderer,
|
||||
Update: self::Update<State, Message>,
|
||||
View: for<'a> self::View<'a, State, Message, Theme, Renderer>,
|
||||
|
|
@ -352,7 +351,7 @@ impl<P: Program> Application<P> {
|
|||
/// Sets the style logic of the [`Application`].
|
||||
pub fn style(
|
||||
self,
|
||||
f: impl Fn(&P::State, &P::Theme) -> Appearance,
|
||||
f: impl Fn(&P::State, &P::Theme) -> theme::Style,
|
||||
) -> Application<
|
||||
impl Program<State = P::State, Message = P::Message, Theme = P::Theme>,
|
||||
> {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
//! Create and run daemons that run in the background.
|
||||
use crate::application;
|
||||
use crate::program::{self, Program};
|
||||
use crate::theme;
|
||||
use crate::window;
|
||||
use crate::{Element, Executor, Font, Result, Settings, Subscription, Task};
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub use crate::shell::program::{Appearance, DefaultStyle};
|
||||
|
||||
/// Creates an iced [`Daemon`] given its title, update, and view logic.
|
||||
///
|
||||
/// A [`Daemon`] will not open a window by default, but will run silently
|
||||
|
|
@ -26,7 +25,7 @@ pub fn daemon<State, Message, Theme, Renderer>(
|
|||
where
|
||||
State: 'static,
|
||||
Message: Send + std::fmt::Debug + 'static,
|
||||
Theme: Default + DefaultStyle,
|
||||
Theme: Default + theme::Base,
|
||||
Renderer: program::Renderer,
|
||||
{
|
||||
use std::marker::PhantomData;
|
||||
|
|
@ -44,7 +43,7 @@ where
|
|||
for Instance<State, Message, Theme, Renderer, Update, View>
|
||||
where
|
||||
Message: Send + std::fmt::Debug + 'static,
|
||||
Theme: Default + DefaultStyle,
|
||||
Theme: Default + theme::Base,
|
||||
Renderer: program::Renderer,
|
||||
Update: application::Update<State, Message>,
|
||||
View: for<'a> self::View<'a, State, Message, Theme, Renderer>,
|
||||
|
|
@ -201,7 +200,7 @@ impl<P: Program> Daemon<P> {
|
|||
/// Sets the style logic of the [`Daemon`].
|
||||
pub fn style(
|
||||
self,
|
||||
f: impl Fn(&P::State, &P::Theme) -> Appearance,
|
||||
f: impl Fn(&P::State, &P::Theme) -> theme::Style,
|
||||
) -> Daemon<
|
||||
impl Program<State = P::State, Message = P::Message, Theme = P::Theme>,
|
||||
> {
|
||||
|
|
|
|||
|
|
@ -491,7 +491,6 @@ mod program;
|
|||
|
||||
pub mod application;
|
||||
pub mod daemon;
|
||||
pub mod settings;
|
||||
pub mod time;
|
||||
pub mod window;
|
||||
|
||||
|
|
@ -506,8 +505,8 @@ pub use crate::core::padding;
|
|||
pub use crate::core::theme;
|
||||
pub use crate::core::{
|
||||
Alignment, Background, Border, Color, ContentFit, Degrees, Gradient,
|
||||
Length, Padding, Pixels, Point, Radians, Rectangle, Rotation, Shadow, Size,
|
||||
Theme, Transformation, Vector,
|
||||
Length, Padding, Pixels, Point, Radians, Rectangle, Rotation, Settings,
|
||||
Shadow, Size, Theme, Transformation, Vector,
|
||||
};
|
||||
pub use crate::runtime::exit;
|
||||
pub use iced_futures::Subscription;
|
||||
|
|
@ -624,8 +623,8 @@ pub use error::Error;
|
|||
pub use event::Event;
|
||||
pub use executor::Executor;
|
||||
pub use font::Font;
|
||||
pub use program::Program;
|
||||
pub use renderer::Renderer;
|
||||
pub use settings::Settings;
|
||||
pub use task::Task;
|
||||
|
||||
#[doc(inline)]
|
||||
|
|
@ -686,7 +685,7 @@ pub fn run<State, Message, Theme, Renderer>(
|
|||
where
|
||||
State: Default + 'static,
|
||||
Message: std::fmt::Debug + Send + 'static,
|
||||
Theme: Default + program::DefaultStyle + 'static,
|
||||
Theme: Default + theme::Base + 'static,
|
||||
Renderer: program::Renderer + 'static,
|
||||
{
|
||||
application(title, update, view).run()
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
use crate::core::text;
|
||||
use crate::graphics::compositor;
|
||||
use crate::shell;
|
||||
use crate::theme;
|
||||
use crate::window;
|
||||
use crate::{Element, Executor, Result, Settings, Subscription, Task};
|
||||
|
||||
pub use crate::shell::program::{Appearance, DefaultStyle};
|
||||
|
||||
/// The internal definition of a [`Program`].
|
||||
///
|
||||
/// You should not need to implement this trait directly. Instead, use the
|
||||
|
|
@ -19,7 +18,7 @@ pub trait Program: Sized {
|
|||
type Message: Send + std::fmt::Debug + 'static;
|
||||
|
||||
/// The theme of the program.
|
||||
type Theme: Default + DefaultStyle;
|
||||
type Theme: Default + theme::Base;
|
||||
|
||||
/// The renderer of the program.
|
||||
type Renderer: Renderer;
|
||||
|
|
@ -51,11 +50,11 @@ pub trait Program: Sized {
|
|||
}
|
||||
|
||||
fn theme(&self, _state: &Self::State, _window: window::Id) -> Self::Theme {
|
||||
Self::Theme::default()
|
||||
<Self::Theme as Default>::default()
|
||||
}
|
||||
|
||||
fn style(&self, _state: &Self::State, theme: &Self::Theme) -> Appearance {
|
||||
DefaultStyle::default_style(theme)
|
||||
fn style(&self, _state: &Self::State, theme: &Self::Theme) -> theme::Style {
|
||||
theme::Base::base(theme)
|
||||
}
|
||||
|
||||
fn scale_factor(&self, _state: &Self::State, _window: window::Id) -> f64 {
|
||||
|
|
@ -153,7 +152,7 @@ pub trait Program: Sized {
|
|||
self.program.theme(&self.state, window)
|
||||
}
|
||||
|
||||
fn style(&self, theme: &Self::Theme) -> Appearance {
|
||||
fn style(&self, theme: &Self::Theme) -> theme::Style {
|
||||
self.program.style(&self.state, theme)
|
||||
}
|
||||
|
||||
|
|
@ -252,7 +251,7 @@ pub fn with_title<P: Program>(
|
|||
&self,
|
||||
state: &Self::State,
|
||||
theme: &Self::Theme,
|
||||
) -> Appearance {
|
||||
) -> theme::Style {
|
||||
self.program.style(state, theme)
|
||||
}
|
||||
|
||||
|
|
@ -322,7 +321,7 @@ pub fn with_subscription<P: Program>(
|
|||
&self,
|
||||
state: &Self::State,
|
||||
theme: &Self::Theme,
|
||||
) -> Appearance {
|
||||
) -> theme::Style {
|
||||
self.program.style(state, theme)
|
||||
}
|
||||
|
||||
|
|
@ -395,7 +394,7 @@ pub fn with_theme<P: Program>(
|
|||
&self,
|
||||
state: &Self::State,
|
||||
theme: &Self::Theme,
|
||||
) -> Appearance {
|
||||
) -> theme::Style {
|
||||
self.program.style(state, theme)
|
||||
}
|
||||
|
||||
|
|
@ -409,7 +408,7 @@ pub fn with_theme<P: Program>(
|
|||
|
||||
pub fn with_style<P: Program>(
|
||||
program: P,
|
||||
f: impl Fn(&P::State, &P::Theme) -> Appearance,
|
||||
f: impl Fn(&P::State, &P::Theme) -> theme::Style,
|
||||
) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> {
|
||||
struct WithStyle<P, F> {
|
||||
program: P,
|
||||
|
|
@ -418,7 +417,7 @@ pub fn with_style<P: Program>(
|
|||
|
||||
impl<P: Program, F> Program for WithStyle<P, F>
|
||||
where
|
||||
F: Fn(&P::State, &P::Theme) -> Appearance,
|
||||
F: Fn(&P::State, &P::Theme) -> theme::Style,
|
||||
{
|
||||
type State = P::State;
|
||||
type Message = P::Message;
|
||||
|
|
@ -430,7 +429,7 @@ pub fn with_style<P: Program>(
|
|||
&self,
|
||||
state: &Self::State,
|
||||
theme: &Self::Theme,
|
||||
) -> Appearance {
|
||||
) -> theme::Style {
|
||||
(self.style)(state, theme)
|
||||
}
|
||||
|
||||
|
|
@ -535,7 +534,7 @@ pub fn with_scale_factor<P: Program>(
|
|||
&self,
|
||||
state: &Self::State,
|
||||
theme: &Self::Theme,
|
||||
) -> Appearance {
|
||||
) -> theme::Style {
|
||||
self.program.style(state, theme)
|
||||
}
|
||||
|
||||
|
|
@ -609,7 +608,7 @@ pub fn with_executor<P: Program, E: Executor>(
|
|||
&self,
|
||||
state: &Self::State,
|
||||
theme: &Self::Theme,
|
||||
) -> Appearance {
|
||||
) -> theme::Style {
|
||||
self.program.style(state, theme)
|
||||
}
|
||||
|
||||
|
|
|
|||
24
test/Cargo.toml
Normal file
24
test/Cargo.toml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
[package]
|
||||
name = "iced_test"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
categories.workspace = true
|
||||
keywords.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
iced_runtime.workspace = true
|
||||
|
||||
iced_renderer.workspace = true
|
||||
iced_renderer.features = ["fira-sans"]
|
||||
|
||||
png.workspace = true
|
||||
sha2.workspace = true
|
||||
thiserror.workspace = true
|
||||
637
test/src/lib.rs
Normal file
637
test/src/lib.rs
Normal file
|
|
@ -0,0 +1,637 @@
|
|||
//! Test your `iced` applications in headless mode.
|
||||
//!
|
||||
//! # Basic Usage
|
||||
//! Let's assume we want to test [the classical counter interface].
|
||||
//!
|
||||
//! First, we will want to create a [`Simulator`] of our interface:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # struct Counter { value: i64 }
|
||||
//! # impl Counter {
|
||||
//! # pub fn view(&self) -> iced_runtime::core::Element<(), iced_runtime::core::Theme, iced_renderer::Renderer> { unimplemented!() }
|
||||
//! # }
|
||||
//! use iced_test::simulator;
|
||||
//!
|
||||
//! let mut counter = Counter { value: 0 };
|
||||
//! let mut ui = simulator(counter.view());
|
||||
//! ```
|
||||
//!
|
||||
//! Now we can simulate a user interacting with our interface. Let's use [`Simulator::click`] to click
|
||||
//! the counter buttons:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # struct Counter { value: i64 }
|
||||
//! # impl Counter {
|
||||
//! # pub fn view(&self) -> iced_runtime::core::Element<(), iced_runtime::core::Theme, iced_renderer::Renderer> { unimplemented!() }
|
||||
//! # }
|
||||
//! use iced_test::selector::text;
|
||||
//! # use iced_test::simulator;
|
||||
//! #
|
||||
//! # let mut counter = Counter { value: 0 };
|
||||
//! # let mut ui = simulator(counter.view());
|
||||
//!
|
||||
//! let _ = ui.click(text("+"));
|
||||
//! let _ = ui.click(text("+"));
|
||||
//! let _ = ui.click(text("-"));
|
||||
//! ```
|
||||
//!
|
||||
//! [`Simulator::click`] takes a [`Selector`]. A [`Selector`] describes a way to query the widgets of an interface. In this case,
|
||||
//! [`selector::text`] lets us select a widget by the text it contains.
|
||||
//!
|
||||
//! We can now process any messages produced by these interactions and then assert that the final value of our counter is
|
||||
//! indeed `1`!
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # struct Counter { value: i64 }
|
||||
//! # impl Counter {
|
||||
//! # pub fn update(&mut self, message: ()) {}
|
||||
//! # pub fn view(&self) -> iced_runtime::core::Element<(), iced_runtime::core::Theme, iced_renderer::Renderer> { unimplemented!() }
|
||||
//! # }
|
||||
//! # use iced_test::selector::text;
|
||||
//! # use iced_test::simulator;
|
||||
//! #
|
||||
//! # let mut counter = Counter { value: 0 };
|
||||
//! # let mut ui = simulator(counter.view());
|
||||
//! #
|
||||
//! # let _ = ui.click(text("+"));
|
||||
//! # let _ = ui.click(text("+"));
|
||||
//! # let _ = ui.click(text("-"));
|
||||
//! #
|
||||
//! for message in ui.into_messages() {
|
||||
//! counter.update(message);
|
||||
//! }
|
||||
//!
|
||||
//! assert_eq!(counter.value, 1);
|
||||
//! ```
|
||||
//!
|
||||
//! We can even rebuild the interface to make sure the counter _displays_ the proper value with [`Simulator::find`]:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # struct Counter { value: i64 }
|
||||
//! # impl Counter {
|
||||
//! # pub fn view(&self) -> iced_runtime::core::Element<(), iced_runtime::core::Theme, iced_renderer::Renderer> { unimplemented!() }
|
||||
//! # }
|
||||
//! # use iced_test::selector::text;
|
||||
//! # use iced_test::simulator;
|
||||
//! #
|
||||
//! # let mut counter = Counter { value: 0 };
|
||||
//! let mut ui = simulator(counter.view());
|
||||
//!
|
||||
//! assert!(ui.find(text("1")).is_ok(), "Counter should display 1!");
|
||||
//! ```
|
||||
//!
|
||||
//! And that's it! That's the gist of testing `iced` applications!
|
||||
//!
|
||||
//! [`Simulator`] contains additional operations you can use to simulate more interactions—like [`tap_key`](Simulator::tap_key) or
|
||||
//! [`typewrite`](Simulator::typewrite)—and even perform [_snapshot testing_](Simulator::snapshot)!
|
||||
//!
|
||||
//! [the classical counter interface]: https://book.iced.rs/architecture.html#dissecting-an-interface
|
||||
pub mod selector;
|
||||
|
||||
pub use selector::Selector;
|
||||
|
||||
use iced_renderer as renderer;
|
||||
use iced_runtime as runtime;
|
||||
use iced_runtime::core;
|
||||
|
||||
use crate::core::clipboard;
|
||||
use crate::core::event;
|
||||
use crate::core::keyboard;
|
||||
use crate::core::mouse;
|
||||
use crate::core::theme;
|
||||
use crate::core::time;
|
||||
use crate::core::widget;
|
||||
use crate::core::window;
|
||||
use crate::core::{
|
||||
Element, Event, Font, Point, Rectangle, Settings, Size, SmolStr,
|
||||
};
|
||||
use crate::runtime::user_interface;
|
||||
use crate::runtime::UserInterface;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Creates a new [`Simulator`].
|
||||
///
|
||||
/// This is just a function version of [`Simulator::new`].
|
||||
pub fn simulator<'a, Message, Theme, Renderer>(
|
||||
element: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Simulator<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: theme::Base,
|
||||
Renderer: core::Renderer + core::renderer::Headless,
|
||||
{
|
||||
Simulator::new(element)
|
||||
}
|
||||
|
||||
/// A user interface that can be interacted with and inspected programmatically.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Simulator<
|
||||
'a,
|
||||
Message,
|
||||
Theme = core::Theme,
|
||||
Renderer = renderer::Renderer,
|
||||
> {
|
||||
raw: UserInterface<'a, Message, Theme, Renderer>,
|
||||
renderer: Renderer,
|
||||
size: Size,
|
||||
cursor: mouse::Cursor,
|
||||
messages: Vec<Message>,
|
||||
}
|
||||
|
||||
/// A specific area of a [`Simulator`], normally containing a widget.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Target {
|
||||
/// The bounds of the area.
|
||||
pub bounds: Rectangle,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Simulator<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: theme::Base,
|
||||
Renderer: core::Renderer + core::renderer::Headless,
|
||||
{
|
||||
/// Creates a new [`Simulator`] with default [`Settings`] and a default size (1024x768).
|
||||
pub fn new(
|
||||
element: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
Self::with_settings(Settings::default(), element)
|
||||
}
|
||||
|
||||
/// Creates a new [`Simulator`] with the given [`Settings`] and a default size (1024x768).
|
||||
pub fn with_settings(
|
||||
settings: Settings,
|
||||
element: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
Self::with_size(settings, window::Settings::default().size, element)
|
||||
}
|
||||
|
||||
/// Creates a new [`Simulator`] with the given [`Settings`] and size.
|
||||
pub fn with_size(
|
||||
settings: Settings,
|
||||
size: impl Into<Size>,
|
||||
element: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
let size = size.into();
|
||||
|
||||
let default_font = match settings.default_font {
|
||||
Font::DEFAULT => Font::with_name("Fira Sans"),
|
||||
_ => settings.default_font,
|
||||
};
|
||||
|
||||
for font in settings.fonts {
|
||||
load_font(font).expect("Font must be valid");
|
||||
}
|
||||
|
||||
let mut renderer =
|
||||
Renderer::new(default_font, settings.default_text_size);
|
||||
|
||||
let raw = UserInterface::build(
|
||||
element,
|
||||
size,
|
||||
user_interface::Cache::default(),
|
||||
&mut renderer,
|
||||
);
|
||||
|
||||
Simulator {
|
||||
raw,
|
||||
renderer,
|
||||
size,
|
||||
cursor: mouse::Cursor::Unavailable,
|
||||
messages: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the [`Target`] of the given widget [`Selector`] in the [`Simulator`].
|
||||
pub fn find(
|
||||
&mut self,
|
||||
selector: impl Into<Selector>,
|
||||
) -> Result<Target, Error> {
|
||||
let selector = selector.into();
|
||||
|
||||
match &selector {
|
||||
Selector::Id(id) => {
|
||||
struct FindById<'a> {
|
||||
id: &'a widget::Id,
|
||||
target: Option<Target>,
|
||||
}
|
||||
|
||||
impl widget::Operation for FindById<'_> {
|
||||
fn container(
|
||||
&mut self,
|
||||
id: Option<&widget::Id>,
|
||||
bounds: Rectangle,
|
||||
operate_on_children: &mut dyn FnMut(
|
||||
&mut dyn widget::Operation<()>,
|
||||
),
|
||||
) {
|
||||
if self.target.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if Some(self.id) == id {
|
||||
self.target = Some(Target { bounds });
|
||||
return;
|
||||
}
|
||||
|
||||
operate_on_children(self);
|
||||
}
|
||||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
id: Option<&widget::Id>,
|
||||
bounds: Rectangle,
|
||||
_content_bounds: Rectangle,
|
||||
_translation: core::Vector,
|
||||
_state: &mut dyn widget::operation::Scrollable,
|
||||
) {
|
||||
if self.target.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if Some(self.id) == id {
|
||||
self.target = Some(Target { bounds });
|
||||
}
|
||||
}
|
||||
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&widget::Id>,
|
||||
bounds: Rectangle,
|
||||
_state: &mut dyn widget::operation::TextInput,
|
||||
) {
|
||||
if self.target.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if Some(self.id) == id {
|
||||
self.target = Some(Target { bounds });
|
||||
}
|
||||
}
|
||||
|
||||
fn text(
|
||||
&mut self,
|
||||
id: Option<&widget::Id>,
|
||||
bounds: Rectangle,
|
||||
_text: &str,
|
||||
) {
|
||||
if self.target.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if Some(self.id) == id {
|
||||
self.target = Some(Target { bounds });
|
||||
}
|
||||
}
|
||||
|
||||
fn custom(
|
||||
&mut self,
|
||||
id: Option<&widget::Id>,
|
||||
bounds: Rectangle,
|
||||
_state: &mut dyn std::any::Any,
|
||||
) {
|
||||
if self.target.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if Some(self.id) == id {
|
||||
self.target = Some(Target { bounds });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut find = FindById { id, target: None };
|
||||
self.raw.operate(&self.renderer, &mut find);
|
||||
|
||||
find.target.ok_or(Error::NotFound(selector))
|
||||
}
|
||||
Selector::Text(text) => {
|
||||
struct FindByText<'a> {
|
||||
text: &'a str,
|
||||
target: Option<Target>,
|
||||
}
|
||||
|
||||
impl widget::Operation for FindByText<'_> {
|
||||
fn container(
|
||||
&mut self,
|
||||
_id: Option<&widget::Id>,
|
||||
_bounds: Rectangle,
|
||||
operate_on_children: &mut dyn FnMut(
|
||||
&mut dyn widget::Operation<()>,
|
||||
),
|
||||
) {
|
||||
if self.target.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
operate_on_children(self);
|
||||
}
|
||||
|
||||
fn text(
|
||||
&mut self,
|
||||
_id: Option<&widget::Id>,
|
||||
bounds: Rectangle,
|
||||
text: &str,
|
||||
) {
|
||||
if self.target.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.text == text {
|
||||
self.target = Some(Target { bounds });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut find = FindByText { text, target: None };
|
||||
self.raw.operate(&self.renderer, &mut find);
|
||||
|
||||
find.target.ok_or(Error::NotFound(selector))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Points the mouse cursor at the given position in the [`Simulator`].
|
||||
///
|
||||
/// This does _not_ produce mouse movement events!
|
||||
pub fn point_at(&mut self, position: impl Into<Point>) {
|
||||
self.cursor = mouse::Cursor::Available(position.into());
|
||||
}
|
||||
|
||||
/// Clicks the [`Target`] found by the given [`Selector`], if any.
|
||||
///
|
||||
/// This consists in:
|
||||
/// - Pointing the mouse cursor at the center of the [`Target`].
|
||||
/// - Simulating a [`click`].
|
||||
pub fn click(
|
||||
&mut self,
|
||||
selector: impl Into<Selector>,
|
||||
) -> Result<Target, Error> {
|
||||
let target = self.find(selector)?;
|
||||
self.point_at(target.bounds.center());
|
||||
|
||||
let _ = self.simulate(click());
|
||||
|
||||
Ok(target)
|
||||
}
|
||||
|
||||
/// Simulates a key press, followed by a release, in the [`Simulator`].
|
||||
pub fn tap_key(&mut self, key: impl Into<keyboard::Key>) -> event::Status {
|
||||
self.simulate(tap_key(key, None))
|
||||
.first()
|
||||
.copied()
|
||||
.unwrap_or(event::Status::Ignored)
|
||||
}
|
||||
|
||||
/// Simulates a user typing in the keyboard the given text in the [`Simulator`].
|
||||
pub fn typewrite(&mut self, text: &str) -> event::Status {
|
||||
let statuses = self.simulate(typewrite(text));
|
||||
|
||||
statuses
|
||||
.into_iter()
|
||||
.fold(event::Status::Ignored, event::Status::merge)
|
||||
}
|
||||
|
||||
/// Simulates the given raw sequence of events in the [`Simulator`].
|
||||
pub fn simulate(
|
||||
&mut self,
|
||||
events: impl IntoIterator<Item = Event>,
|
||||
) -> Vec<event::Status> {
|
||||
let events: Vec<Event> = events.into_iter().collect();
|
||||
|
||||
let (_state, statuses) = self.raw.update(
|
||||
&events,
|
||||
self.cursor,
|
||||
&mut self.renderer,
|
||||
&mut clipboard::Null,
|
||||
&mut self.messages,
|
||||
);
|
||||
|
||||
statuses
|
||||
}
|
||||
|
||||
/// Draws and takes a [`Snapshot`] of the interface in the [`Simulator`].
|
||||
pub fn snapshot(&mut self, theme: &Theme) -> Result<Snapshot, Error> {
|
||||
let base = theme.base();
|
||||
|
||||
let _ = self.raw.update(
|
||||
&[Event::Window(window::Event::RedrawRequested(
|
||||
time::Instant::now(),
|
||||
))],
|
||||
self.cursor,
|
||||
&mut self.renderer,
|
||||
&mut clipboard::Null,
|
||||
&mut self.messages,
|
||||
);
|
||||
|
||||
let _ = self.raw.draw(
|
||||
&mut self.renderer,
|
||||
theme,
|
||||
&core::renderer::Style {
|
||||
text_color: base.text_color,
|
||||
},
|
||||
self.cursor,
|
||||
);
|
||||
|
||||
let scale_factor = 2.0;
|
||||
|
||||
let physical_size = Size::new(
|
||||
(self.size.width * scale_factor).round() as u32,
|
||||
(self.size.height * scale_factor).round() as u32,
|
||||
);
|
||||
|
||||
let rgba = self.renderer.screenshot(
|
||||
physical_size,
|
||||
scale_factor,
|
||||
base.background_color,
|
||||
);
|
||||
|
||||
Ok(Snapshot {
|
||||
screenshot: window::Screenshot::new(
|
||||
rgba,
|
||||
physical_size,
|
||||
f64::from(scale_factor),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
/// Turns the [`Simulator`] into the sequence of messages produced by any interactions.
|
||||
pub fn into_messages(self) -> impl Iterator<Item = Message> {
|
||||
self.messages.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// A frame of a user interface rendered by a [`Simulator`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Snapshot {
|
||||
screenshot: window::Screenshot,
|
||||
}
|
||||
|
||||
impl Snapshot {
|
||||
/// Compares the [`Snapshot`] with the PNG image found in the given path, returning
|
||||
/// `true` if they are identical.
|
||||
///
|
||||
/// If the PNG image does not exist, it will be created by the [`Snapshot`] for future
|
||||
/// testing and `true` will be returned.
|
||||
pub fn matches_image(&self, path: impl AsRef<Path>) -> Result<bool, Error> {
|
||||
let path = snapshot_path(path, "png");
|
||||
|
||||
if path.exists() {
|
||||
let file = fs::File::open(&path)?;
|
||||
let decoder = png::Decoder::new(file);
|
||||
|
||||
let mut reader = decoder.read_info()?;
|
||||
let mut bytes = vec![0; reader.output_buffer_size()];
|
||||
let info = reader.next_frame(&mut bytes)?;
|
||||
|
||||
Ok(self.screenshot.bytes == bytes[..info.buffer_size()])
|
||||
} else {
|
||||
if let Some(directory) = path.parent() {
|
||||
fs::create_dir_all(directory)?;
|
||||
}
|
||||
|
||||
let file = fs::File::create(path)?;
|
||||
|
||||
let mut encoder = png::Encoder::new(
|
||||
file,
|
||||
self.screenshot.size.width,
|
||||
self.screenshot.size.height,
|
||||
);
|
||||
encoder.set_color(png::ColorType::Rgba);
|
||||
|
||||
let mut writer = encoder.write_header()?;
|
||||
writer.write_image_data(&self.screenshot.bytes)?;
|
||||
writer.finish()?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Compares the [`Snapshot`] with the SHA-256 hash file found in the given path, returning
|
||||
/// `true` if they are identical.
|
||||
///
|
||||
/// If the hash file does not exist, it will be created by the [`Snapshot`] for future
|
||||
/// testing and `true` will be returned.
|
||||
pub fn matches_hash(&self, path: impl AsRef<Path>) -> Result<bool, Error> {
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
let path = snapshot_path(path, "sha256");
|
||||
|
||||
let hash = {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&self.screenshot.bytes);
|
||||
format!("{:x}", hasher.finalize())
|
||||
};
|
||||
|
||||
if path.exists() {
|
||||
let saved_hash = fs::read_to_string(&path)?;
|
||||
|
||||
Ok(hash == saved_hash)
|
||||
} else {
|
||||
if let Some(directory) = path.parent() {
|
||||
fs::create_dir_all(directory)?;
|
||||
}
|
||||
|
||||
fs::write(path, hash)?;
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the sequence of events of a click.
|
||||
pub fn click() -> impl Iterator<Item = Event> {
|
||||
[
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
||||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
/// Returns the sequence of events of a "key tap" (i.e. pressing and releasing a key).
|
||||
pub fn tap_key(
|
||||
key: impl Into<keyboard::Key>,
|
||||
text: Option<SmolStr>,
|
||||
) -> impl Iterator<Item = Event> {
|
||||
let key = key.into();
|
||||
|
||||
[
|
||||
Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key: key.clone(),
|
||||
modified_key: key.clone(),
|
||||
physical_key: keyboard::key::Physical::Unidentified(
|
||||
keyboard::key::NativeCode::Unidentified,
|
||||
),
|
||||
location: keyboard::Location::Standard,
|
||||
modifiers: keyboard::Modifiers::default(),
|
||||
text,
|
||||
}),
|
||||
Event::Keyboard(keyboard::Event::KeyReleased {
|
||||
key: key.clone(),
|
||||
modified_key: key,
|
||||
physical_key: keyboard::key::Physical::Unidentified(
|
||||
keyboard::key::NativeCode::Unidentified,
|
||||
),
|
||||
location: keyboard::Location::Standard,
|
||||
modifiers: keyboard::Modifiers::default(),
|
||||
}),
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
/// Returns the sequence of events of typewriting the given text in a keyboard.
|
||||
pub fn typewrite(text: &str) -> impl Iterator<Item = Event> + '_ {
|
||||
text.chars()
|
||||
.map(|c| SmolStr::new_inline(&c.to_string()))
|
||||
.flat_map(|c| tap_key(keyboard::Key::Character(c.clone()), Some(c)))
|
||||
}
|
||||
|
||||
/// A test error.
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// No matching widget was found for the [`Selector`].
|
||||
#[error("no matching widget was found for the selector: {0:?}")]
|
||||
NotFound(Selector),
|
||||
/// An IO operation failed.
|
||||
#[error("an IO operation failed: {0}")]
|
||||
IOFailed(Arc<io::Error>),
|
||||
/// The decoding of some PNG image failed.
|
||||
#[error("the decoding of some PNG image failed: {0}")]
|
||||
PngDecodingFailed(Arc<png::DecodingError>),
|
||||
/// The encoding of some PNG image failed.
|
||||
#[error("the encoding of some PNG image failed: {0}")]
|
||||
PngEncodingFailed(Arc<png::EncodingError>),
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(error: io::Error) -> Self {
|
||||
Self::IOFailed(Arc::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<png::DecodingError> for Error {
|
||||
fn from(error: png::DecodingError) -> Self {
|
||||
Self::PngDecodingFailed(Arc::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<png::EncodingError> for Error {
|
||||
fn from(error: png::EncodingError) -> Self {
|
||||
Self::PngEncodingFailed(Arc::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
fn load_font(font: impl Into<Cow<'static, [u8]>>) -> Result<(), Error> {
|
||||
renderer::graphics::text::font_system()
|
||||
.write()
|
||||
.expect("Write to font system")
|
||||
.load_font(font.into());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn snapshot_path(path: impl AsRef<Path>, extension: &str) -> PathBuf {
|
||||
path.as_ref().with_extension(extension)
|
||||
}
|
||||
29
test/src/selector.rs
Normal file
29
test/src/selector.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
//! Select widgets of a user interface.
|
||||
use crate::core::text;
|
||||
use crate::core::widget;
|
||||
|
||||
/// A selector describes a strategy to find a certain widget in a user interface.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Selector {
|
||||
/// Find the widget with the given [`widget::Id`].
|
||||
Id(widget::Id),
|
||||
/// Find the widget containing the given [`text::Fragment`].
|
||||
Text(text::Fragment<'static>),
|
||||
}
|
||||
|
||||
impl From<widget::Id> for Selector {
|
||||
fn from(id: widget::Id) -> Self {
|
||||
Self::Id(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Selector {
|
||||
fn from(id: &'static str) -> Self {
|
||||
Self::Id(widget::Id::new(id))
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates [`Selector`] that finds the widget containing the given text fragment.
|
||||
pub fn text(fragment: impl text::IntoFragment<'static>) -> Selector {
|
||||
Selector::Text(fragment.into_fragment())
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ pub use geometry::Geometry;
|
|||
|
||||
use crate::core::renderer;
|
||||
use crate::core::{
|
||||
Background, Color, Font, Pixels, Point, Rectangle, Transformation,
|
||||
Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation,
|
||||
};
|
||||
use crate::engine::Engine;
|
||||
use crate::graphics::compositor;
|
||||
|
|
@ -405,3 +405,26 @@ impl core::svg::Renderer for Renderer {
|
|||
impl compositor::Default for Renderer {
|
||||
type Compositor = window::Compositor;
|
||||
}
|
||||
|
||||
impl renderer::Headless for Renderer {
|
||||
fn new(default_font: Font, default_text_size: Pixels) -> Self {
|
||||
Self::new(default_font, default_text_size)
|
||||
}
|
||||
|
||||
fn screenshot(
|
||||
&mut self,
|
||||
size: Size<u32>,
|
||||
scale_factor: f32,
|
||||
background_color: Color,
|
||||
) -> Vec<u8> {
|
||||
let viewport =
|
||||
Viewport::with_physical_size(size, f64::from(scale_factor));
|
||||
|
||||
window::compositor::screenshot::<&str>(
|
||||
self,
|
||||
&viewport,
|
||||
background_color,
|
||||
&[],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -445,6 +445,16 @@ where
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
_state: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
_renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
operation.text(None, layout.bounds(), &self.label);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> From<Checkbox<'a, Message, Theme, Renderer>>
|
||||
|
|
|
|||
|
|
@ -493,11 +493,11 @@ pub fn visible_bounds(id: Id) -> Task<Option<Rectangle>> {
|
|||
impl Operation<Option<Rectangle>> for VisibleBounds {
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
_state: &mut dyn widget::operation::Scrollable,
|
||||
_id: Option<&widget::Id>,
|
||||
bounds: Rectangle,
|
||||
_content_bounds: Rectangle,
|
||||
translation: Vector,
|
||||
_state: &mut dyn widget::operation::Scrollable,
|
||||
) {
|
||||
match self.scrollables.last() {
|
||||
Some((last_translation, last_viewport, _depth)) => {
|
||||
|
|
|
|||
|
|
@ -487,11 +487,11 @@ where
|
|||
state.translation(self.direction, bounds, content_bounds);
|
||||
|
||||
operation.scrollable(
|
||||
state,
|
||||
self.id.as_ref().map(|id| &id.0),
|
||||
bounds,
|
||||
content_bounds,
|
||||
translation,
|
||||
state,
|
||||
);
|
||||
|
||||
operation.container(
|
||||
|
|
|
|||
|
|
@ -971,13 +971,13 @@ where
|
|||
fn operate(
|
||||
&self,
|
||||
tree: &mut widget::Tree,
|
||||
_layout: Layout<'_>,
|
||||
layout: Layout<'_>,
|
||||
_renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
let state = tree.state.downcast_mut::<State<Highlighter>>();
|
||||
|
||||
operation.focusable(state, None);
|
||||
operation.focusable(None, layout.bounds(), state);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -617,14 +617,23 @@ where
|
|||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
_layout: Layout<'_>,
|
||||
layout: Layout<'_>,
|
||||
_renderer: &Renderer,
|
||||
operation: &mut dyn Operation,
|
||||
) {
|
||||
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
|
||||
|
||||
operation.focusable(state, self.id.as_ref().map(|id| &id.0));
|
||||
operation.text_input(state, self.id.as_ref().map(|id| &id.0));
|
||||
operation.focusable(
|
||||
self.id.as_ref().map(|id| &id.0),
|
||||
layout.bounds(),
|
||||
state,
|
||||
);
|
||||
|
||||
operation.text_input(
|
||||
self.id.as_ref().map(|id| &id.0),
|
||||
layout.bounds(),
|
||||
state,
|
||||
);
|
||||
}
|
||||
|
||||
fn update(
|
||||
|
|
|
|||
|
|
@ -8,10 +8,11 @@ use crate::conversion;
|
|||
use crate::core;
|
||||
use crate::core::mouse;
|
||||
use crate::core::renderer;
|
||||
use crate::core::theme;
|
||||
use crate::core::time::Instant;
|
||||
use crate::core::widget::operation;
|
||||
use crate::core::window;
|
||||
use crate::core::{Color, Element, Point, Size, Theme};
|
||||
use crate::core::{Element, Point, Size};
|
||||
use crate::futures::futures::channel::mpsc;
|
||||
use crate::futures::futures::channel::oneshot;
|
||||
use crate::futures::futures::task;
|
||||
|
|
@ -46,7 +47,7 @@ use std::sync::Arc;
|
|||
pub trait Program
|
||||
where
|
||||
Self: Sized,
|
||||
Self::Theme: DefaultStyle,
|
||||
Self::Theme: theme::Base,
|
||||
{
|
||||
/// The type of __messages__ your [`Program`] will produce.
|
||||
type Message: std::fmt::Debug + Send;
|
||||
|
|
@ -106,8 +107,8 @@ where
|
|||
fn theme(&self, window: window::Id) -> Self::Theme;
|
||||
|
||||
/// Returns the `Style` variation of the `Theme`.
|
||||
fn style(&self, theme: &Self::Theme) -> Appearance {
|
||||
theme.default_style()
|
||||
fn style(&self, theme: &Self::Theme) -> theme::Style {
|
||||
theme::Base::base(theme)
|
||||
}
|
||||
|
||||
/// Returns the event `Subscription` for the current state of the
|
||||
|
|
@ -138,37 +139,6 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// The appearance of a program.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Appearance {
|
||||
/// The background [`Color`] of the application.
|
||||
pub background_color: Color,
|
||||
|
||||
/// The default text [`Color`] of the application.
|
||||
pub text_color: Color,
|
||||
}
|
||||
|
||||
/// The default style of a [`Program`].
|
||||
pub trait DefaultStyle {
|
||||
/// Returns the default style of a [`Program`].
|
||||
fn default_style(&self) -> Appearance;
|
||||
}
|
||||
|
||||
impl DefaultStyle for Theme {
|
||||
fn default_style(&self) -> Appearance {
|
||||
default(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// The default [`Appearance`] of a [`Program`] with the built-in [`Theme`].
|
||||
pub fn default(theme: &Theme) -> Appearance {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
Appearance {
|
||||
background_color: palette.background.base.color,
|
||||
text_color: palette.background.base.text,
|
||||
}
|
||||
}
|
||||
/// Runs a [`Program`] with an executor, compositor, and the provided
|
||||
/// settings.
|
||||
pub fn run<P, C>(
|
||||
|
|
@ -180,7 +150,7 @@ pub fn run<P, C>(
|
|||
where
|
||||
P: Program + 'static,
|
||||
C: Compositor<Renderer = P::Renderer> + 'static,
|
||||
P::Theme: DefaultStyle,
|
||||
P::Theme: theme::Base,
|
||||
{
|
||||
use winit::event_loop::EventLoop;
|
||||
|
||||
|
|
@ -674,7 +644,7 @@ async fn run_instance<P, C>(
|
|||
) where
|
||||
P: Program + 'static,
|
||||
C: Compositor<Renderer = P::Renderer> + 'static,
|
||||
P::Theme: DefaultStyle,
|
||||
P::Theme: theme::Base,
|
||||
{
|
||||
use winit::event;
|
||||
use winit::event_loop::ControlFlow;
|
||||
|
|
@ -1170,7 +1140,7 @@ fn build_user_interface<'a, P: Program>(
|
|||
id: window::Id,
|
||||
) -> UserInterface<'a, P::Message, P::Theme, P::Renderer>
|
||||
where
|
||||
P::Theme: DefaultStyle,
|
||||
P::Theme: theme::Base,
|
||||
{
|
||||
debug.view_started();
|
||||
let view = program.view(id);
|
||||
|
|
@ -1189,7 +1159,7 @@ fn update<P: Program, E: Executor>(
|
|||
debug: &mut Debug,
|
||||
messages: &mut Vec<P::Message>,
|
||||
) where
|
||||
P::Theme: DefaultStyle,
|
||||
P::Theme: theme::Base,
|
||||
{
|
||||
for message in messages.drain(..) {
|
||||
debug.log_message(&message);
|
||||
|
|
@ -1226,7 +1196,7 @@ fn run_action<P, C>(
|
|||
) where
|
||||
P: Program,
|
||||
C: Compositor<Renderer = P::Renderer> + 'static,
|
||||
P::Theme: DefaultStyle,
|
||||
P::Theme: theme::Base,
|
||||
{
|
||||
use crate::runtime::clipboard;
|
||||
use crate::runtime::system;
|
||||
|
|
@ -1461,7 +1431,7 @@ fn run_action<P, C>(
|
|||
&debug.overlay(),
|
||||
);
|
||||
|
||||
let _ = channel.send(window::Screenshot::new(
|
||||
let _ = channel.send(core::window::Screenshot::new(
|
||||
bytes,
|
||||
window.state.physical_size(),
|
||||
window.state.viewport().scale_factor(),
|
||||
|
|
@ -1536,7 +1506,7 @@ pub fn build_user_interfaces<'a, P: Program, C>(
|
|||
) -> FxHashMap<window::Id, UserInterface<'a, P::Message, P::Theme, P::Renderer>>
|
||||
where
|
||||
C: Compositor<Renderer = P::Renderer>,
|
||||
P::Theme: DefaultStyle,
|
||||
P::Theme: theme::Base,
|
||||
{
|
||||
cached_user_interfaces
|
||||
.drain()
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
use crate::conversion;
|
||||
use crate::core::{mouse, window};
|
||||
use crate::core::{mouse, theme, window};
|
||||
use crate::core::{Color, Size};
|
||||
use crate::graphics::Viewport;
|
||||
use crate::program::{self, Program};
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use crate::program::Program;
|
||||
|
||||
use winit::event::{Touch, WindowEvent};
|
||||
use winit::window::Window;
|
||||
|
||||
use std::fmt::{Debug, Formatter};
|
||||
|
||||
/// The state of a multi-windowed [`Program`].
|
||||
pub struct State<P: Program>
|
||||
where
|
||||
P::Theme: program::DefaultStyle,
|
||||
P::Theme: theme::Base,
|
||||
{
|
||||
title: String,
|
||||
scale_factor: f64,
|
||||
|
|
@ -20,12 +21,12 @@ where
|
|||
cursor_position: Option<winit::dpi::PhysicalPosition<f64>>,
|
||||
modifiers: winit::keyboard::ModifiersState,
|
||||
theme: P::Theme,
|
||||
appearance: program::Appearance,
|
||||
style: theme::Style,
|
||||
}
|
||||
|
||||
impl<P: Program> Debug for State<P>
|
||||
where
|
||||
P::Theme: program::DefaultStyle,
|
||||
P::Theme: theme::Base,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("multi_window::State")
|
||||
|
|
@ -34,14 +35,14 @@ where
|
|||
.field("viewport", &self.viewport)
|
||||
.field("viewport_version", &self.viewport_version)
|
||||
.field("cursor_position", &self.cursor_position)
|
||||
.field("appearance", &self.appearance)
|
||||
.field("style", &self.style)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Program> State<P>
|
||||
where
|
||||
P::Theme: program::DefaultStyle,
|
||||
P::Theme: theme::Base,
|
||||
{
|
||||
/// Creates a new [`State`] for the provided [`Program`]'s `window`.
|
||||
pub fn new(
|
||||
|
|
@ -52,7 +53,7 @@ where
|
|||
let title = application.title(window_id);
|
||||
let scale_factor = application.scale_factor(window_id);
|
||||
let theme = application.theme(window_id);
|
||||
let appearance = application.style(&theme);
|
||||
let style = application.style(&theme);
|
||||
|
||||
let viewport = {
|
||||
let physical_size = window.inner_size();
|
||||
|
|
@ -71,7 +72,7 @@ where
|
|||
cursor_position: None,
|
||||
modifiers: winit::keyboard::ModifiersState::default(),
|
||||
theme,
|
||||
appearance,
|
||||
style,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -127,12 +128,12 @@ where
|
|||
|
||||
/// Returns the current background [`Color`] of the [`State`].
|
||||
pub fn background_color(&self) -> Color {
|
||||
self.appearance.background_color
|
||||
self.style.background_color
|
||||
}
|
||||
|
||||
/// Returns the current text [`Color`] of the [`State`].
|
||||
pub fn text_color(&self) -> Color {
|
||||
self.appearance.text_color
|
||||
self.style.text_color
|
||||
}
|
||||
|
||||
/// Processes the provided window event and updates the [`State`] accordingly.
|
||||
|
|
@ -237,6 +238,6 @@ where
|
|||
|
||||
// Update theme and appearance
|
||||
self.theme = application.theme(window_id);
|
||||
self.appearance = application.style(&self.theme);
|
||||
self.style = application.style(&self.theme);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
use crate::core::mouse;
|
||||
use crate::core::theme;
|
||||
use crate::core::time::Instant;
|
||||
use crate::core::window::Id;
|
||||
use crate::core::{Point, Size};
|
||||
use crate::graphics::Compositor;
|
||||
use crate::program::{DefaultStyle, Program, State};
|
||||
use crate::program::{Program, State};
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -14,7 +15,7 @@ pub struct WindowManager<P, C>
|
|||
where
|
||||
P: Program,
|
||||
C: Compositor<Renderer = P::Renderer>,
|
||||
P::Theme: DefaultStyle,
|
||||
P::Theme: theme::Base,
|
||||
{
|
||||
aliases: BTreeMap<winit::window::WindowId, Id>,
|
||||
entries: BTreeMap<Id, Window<P, C>>,
|
||||
|
|
@ -24,7 +25,7 @@ impl<P, C> WindowManager<P, C>
|
|||
where
|
||||
P: Program,
|
||||
C: Compositor<Renderer = P::Renderer>,
|
||||
P::Theme: DefaultStyle,
|
||||
P::Theme: theme::Base,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
|
@ -132,7 +133,7 @@ impl<P, C> Default for WindowManager<P, C>
|
|||
where
|
||||
P: Program,
|
||||
C: Compositor<Renderer = P::Renderer>,
|
||||
P::Theme: DefaultStyle,
|
||||
P::Theme: theme::Base,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
|
|
@ -144,7 +145,7 @@ pub struct Window<P, C>
|
|||
where
|
||||
P: Program,
|
||||
C: Compositor<Renderer = P::Renderer>,
|
||||
P::Theme: DefaultStyle,
|
||||
P::Theme: theme::Base,
|
||||
{
|
||||
pub raw: Arc<winit::window::Window>,
|
||||
pub state: State<P>,
|
||||
|
|
@ -160,7 +161,7 @@ impl<P, C> Window<P, C>
|
|||
where
|
||||
P: Program,
|
||||
C: Compositor<Renderer = P::Renderer>,
|
||||
P::Theme: DefaultStyle,
|
||||
P::Theme: theme::Base,
|
||||
{
|
||||
pub fn position(&self) -> Option<Point> {
|
||||
self.raw
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
//! Configure your application.
|
||||
use crate::core;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// The settings of an application.
|
||||
|
|
@ -13,3 +15,12 @@ pub struct Settings {
|
|||
/// The fonts to load on boot.
|
||||
pub fonts: Vec<Cow<'static, [u8]>>,
|
||||
}
|
||||
|
||||
impl From<core::Settings> for Settings {
|
||||
fn from(settings: core::Settings) -> Self {
|
||||
Self {
|
||||
id: settings.id,
|
||||
fonts: settings.fonts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue