Files
galaxy/crates/onboarding/src/visuals/onboarding_visual.rs
T

359 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use galaxy_core::ui::Icon;
use galaxyui_core::assets::asset_cache::{AssetCache, AssetSource, AssetState};
use galaxyui_core::elements::{CornerRadius, Fill, Point, Radius};
use galaxyui_core::event::DispatchedEvent;
use galaxyui_core::geometry::rect::RectF;
use galaxyui_core::geometry::vector::{vec2f, Vector2F};
use galaxyui_core::image_cache::{AnimatedImageBehavior, CacheOption, FitType, Image, ImageCache};
use galaxyui_core::{
AfterLayoutContext, AppContext, Element, EventContext, LayoutContext, PaintContext,
SingletonEntity as _, SizeConstraint,
};
use pathfinder_color::ColorU;
#[derive(Debug, Clone, Copy)]
pub(crate) struct RectPct {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
}
impl RectPct {
pub const fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
Self { x, y, w, h }
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Pill {
pub rect: RectPct,
pub color: ColorU,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Rect {
pub rect: RectPct,
pub color: ColorU,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct IconPct {
pub icon: Icon,
pub color: ColorU,
pub center_x: f32,
pub center_y: f32,
pub width_pct: f32,
}
pub(crate) struct OnboardingVisual {
panel_background: ColorU,
pills: Vec<Pill>,
rects: Vec<Rect>,
icons: Vec<IconPct>,
terminal_box: bool,
size: Option<Vector2F>,
origin: Option<Point>,
}
impl OnboardingVisual {
// Padding scale parameters (based on max(x, y) in pixels).
const MIN_PADDING_SIZE_PX: f32 = 500.0;
const MAX_PADDING_SIZE_PX: f32 = 2000.0;
const MIN_PADDING_PCT: f32 = 0.10;
const MAX_PADDING_PCT: f32 = 0.30;
pub fn new(panel_background: ColorU, pills: Vec<Pill>, terminal_box: bool) -> Self {
Self::new_internal(panel_background, pills, terminal_box)
}
fn new_internal(panel_background: ColorU, pills: Vec<Pill>, terminal_box: bool) -> Self {
Self {
panel_background,
pills,
icons: Vec::new(),
rects: Vec::new(),
terminal_box,
size: None,
origin: None,
}
}
pub fn with_icons(mut self, icons: Vec<IconPct>) -> Self {
self.icons = icons;
self
}
pub fn with_rects(mut self, rects: Vec<Rect>) -> Self {
self.rects = rects;
self
}
pub fn compute_contained_size(constraint: SizeConstraint) -> Vector2F {
// Maintain the old images aspect ratio so the right-side layout stays stable.
const ASPECT_RATIO: f32 = 1.5;
let max = constraint.max;
if max.x().is_infinite() && max.y().is_infinite() {
return vec2f(constraint.min.x().max(0.), constraint.min.y().max(0.));
}
let mut width = if max.x().is_finite() {
max.x().max(0.)
} else {
(max.y().max(0.)) * ASPECT_RATIO
};
let mut height = if max.y().is_finite() {
max.y().max(0.)
} else {
width / ASPECT_RATIO
};
// Contain within both axes when both are finite.
if max.x().is_finite() && max.y().is_finite() {
height = width / ASPECT_RATIO;
if height > max.y() {
height = max.y();
width = height * ASPECT_RATIO;
}
}
vec2f(width.max(1.), height.max(1.))
}
fn feature_radius_px(inner_height: f32) -> f32 {
// All features use the same radius.
inner_height * 0.02
}
fn draw_pill(
&self,
rect: RectF,
color: ColorU,
feature_radius_px: f32,
ctx: &mut PaintContext,
) {
ctx.scene
.draw_rect_with_hit_recording(rect)
.with_background(Fill::Solid(color))
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(feature_radius_px)));
}
fn draw_rect(&self, rect: RectF, color: ColorU, ctx: &mut PaintContext) {
ctx.scene
.draw_rect_with_hit_recording(rect)
.with_background(Fill::Solid(color));
}
fn rect_from_pct(inner_origin: Vector2F, inner_size: Vector2F, rect: RectPct) -> RectF {
RectF::new(
vec2f(
inner_origin.x() + inner_size.x() * rect.x,
inner_origin.y() + inner_size.y() * rect.y,
),
vec2f(inner_size.x() * rect.w, inner_size.y() * rect.h),
)
}
fn icon_rect_from_pct(
inner_origin: Vector2F,
inner_size: Vector2F,
center_x: f32,
center_y: f32,
width_pct: f32,
) -> RectF {
let center = vec2f(
inner_origin.x() + inner_size.x() * center_x,
inner_origin.y() + inner_size.y() * center_y,
);
let width_px = inner_size.x() * width_pct;
RectF::new(
center - vec2f(width_px / 2.0, width_px / 2.0),
vec2f(width_px, width_px),
)
}
fn draw_icon(
&self,
rect: RectF,
icon: Icon,
color: ColorU,
ctx: &mut PaintContext,
app: &AppContext,
) {
let bounds = (rect.size() * ctx.scene.scale_factor()).to_i32();
if bounds.x() <= 0 || bounds.y() <= 0 {
return;
}
let path: &'static str = icon.into();
let asset_cache = AssetCache::as_ref(app);
match ImageCache::as_ref(app).image(
AssetSource::Bundled { path },
bounds,
FitType::Contain,
AnimatedImageBehavior::FullAnimation,
CacheOption::BySize,
ctx.max_texture_dimension_2d,
asset_cache,
) {
AssetState::Loaded { data } => match data.as_ref() {
Image::Static(image) => {
let logical_image_size = image.size().to_f32() / ctx.scene.scale_factor();
let origin = rect.origin() + ((rect.size() - logical_image_size) / 2.0);
ctx.scene.draw_icon(
RectF::new(origin, logical_image_size),
image.clone(),
1.0,
color,
);
}
Image::Animated(_) => {
log::info!("Animated icons are currently not supported");
}
},
AssetState::Loading { handle } => {
ctx.repaint_after_load(handle);
}
AssetState::Evicted => {
log::warn!("Unable to render svg because it was evicted");
}
AssetState::FailedToLoad(err) => {
log::warn!("Unable to render svg: {err:#}");
}
}
}
}
impl Element for OnboardingVisual {
fn layout(
&mut self,
constraint: SizeConstraint,
_ctx: &mut LayoutContext,
_app: &AppContext,
) -> Vector2F {
let size = Self::compute_contained_size(constraint);
self.size = Some(size);
size
}
fn after_layout(&mut self, _ctx: &mut AfterLayoutContext, _app: &AppContext) {}
fn paint(&mut self, origin: Vector2F, ctx: &mut PaintContext, _app: &AppContext) {
let Some(size) = self.size else {
return;
};
self.origin = Some(Point::from_vec2f(origin, ctx.scene.z_index()));
let max_dimension = size.x().max(size.y());
// Scale padding linearly with size.
let padding_pct = if max_dimension <= Self::MIN_PADDING_SIZE_PX {
Self::MIN_PADDING_PCT
} else if max_dimension >= Self::MAX_PADDING_SIZE_PX {
Self::MAX_PADDING_PCT
} else {
let t = (max_dimension - Self::MIN_PADDING_SIZE_PX)
/ (Self::MAX_PADDING_SIZE_PX - Self::MIN_PADDING_SIZE_PX);
Self::MIN_PADDING_PCT + t * (Self::MAX_PADDING_PCT - Self::MIN_PADDING_PCT)
};
let padding_x: f32 = padding_pct * size.x();
let padding_y: f32 = padding_pct * size.y();
let inner_origin = origin + vec2f(padding_x, padding_y);
let inner_size = vec2f(
(size.x() - 2.0 * padding_x).max(1.),
(size.y() - 2.0 * padding_y).max(1.),
);
let feature_radius_px = Self::feature_radius_px(inner_size.y());
// Background panel.
ctx.scene
.draw_rect_with_hit_recording(RectF::new(inner_origin, inner_size))
.with_background(Fill::Solid(self.panel_background))
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(feature_radius_px)));
if self.terminal_box {
// Bottom container behind the final rows.
const TERMINAL_BOX_Y_PCT: f32 = 0.80;
const TERMINAL_BOX_H_PCT: f32 = 0.25;
let min_x = self
.pills
.iter()
.map(|pill| pill.rect.x)
.fold(0.0_f32, f32::min);
let max_x = self
.pills
.iter()
.map(|pill| pill.rect.x + pill.rect.w)
.fold(1.0_f32, f32::max);
let box_rect = Self::rect_from_pct(
inner_origin,
inner_size,
RectPct::new(min_x, TERMINAL_BOX_Y_PCT, max_x - min_x, TERMINAL_BOX_H_PCT),
);
let box_color = self
.pills
.first()
.map(|pill| pill.color)
.unwrap_or(self.panel_background);
ctx.scene
.draw_rect_with_hit_recording(box_rect)
.with_background(Fill::Solid(box_color))
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(feature_radius_px)));
}
// Rects.
for rect in &self.rects {
let rect_px = Self::rect_from_pct(inner_origin, inner_size, rect.rect);
self.draw_rect(rect_px, rect.color, ctx);
}
// Pills.
for pill in &self.pills {
let rect = Self::rect_from_pct(inner_origin, inner_size, pill.rect);
self.draw_pill(rect, pill.color, feature_radius_px, ctx);
}
// Icons.
for icon in &self.icons {
let rect = Self::icon_rect_from_pct(
inner_origin,
inner_size,
icon.center_x,
icon.center_y,
icon.width_pct,
);
self.draw_icon(rect, icon.icon, icon.color, ctx, _app);
}
}
fn dispatch_event(
&mut self,
_event: &DispatchedEvent,
_ctx: &mut EventContext,
_app: &AppContext,
) -> bool {
false
}
fn size(&self) -> Option<Vector2F> {
self.size
}
fn origin(&self) -> Option<Point> {
self.origin
}
}