pub struct LayoutSize {
pub width: LayoutDimension,
pub height: LayoutDimension,
}Fields§
§width: LayoutDimension§height: LayoutDimensionImplementations§
Source§impl LayoutSize
impl LayoutSize
pub const AUTO: Self
pub const ZERO: Self
pub const FILL: Self
Sourcepub const fn new(width: LayoutDimension, height: LayoutDimension) -> Self
pub const fn new(width: LayoutDimension, height: LayoutDimension) -> Self
Examples found in repository?
examples/showcase.rs (lines 6822-6825)
6783fn menu_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6784 let body = section_with_min_viewport(
6785 ui,
6786 parent,
6787 "menus",
6788 "Menu controls",
6789 UiSize::new(320.0, 0.0),
6790 );
6791 let menus = menu_bar_menus(state.menu_autosave, state.menu_grid);
6792 let active_items = state
6793 .menu_bar
6794 .open_menu
6795 .and_then(|index| menus.get(index))
6796 .map(|menu| menu.items.clone())
6797 .unwrap_or_default();
6798 widgets::label(
6799 ui,
6800 body,
6801 "menus.menu_bar.title",
6802 "Menu bar",
6803 text(12.0, color(166, 176, 190)),
6804 LayoutStyle::new().with_width_percent(1.0),
6805 );
6806 ext_widgets::menu_bar(
6807 ui,
6808 body,
6809 "menus.menu_bar",
6810 &menus,
6811 &state.menu_bar,
6812 None,
6813 ext_widgets::MenuBarOptions::default().with_action_prefix("menus.bar"),
6814 );
6815
6816 if !active_items.is_empty() {
6817 let menu_columns = ui.add_child(
6818 body,
6819 UiNode::container(
6820 "menus.menu_columns",
6821 Layout::row()
6822 .size(LayoutSize::new(
6823 LayoutDimension::Auto,
6824 LayoutDimension::Auto,
6825 ))
6826 .align_items(LayoutAlignment::Start)
6827 .gap(LayoutGap::points(4.0, 4.0))
6828 .flex(0.0, 0.0, LayoutDimension::Auto)
6829 .to_layout_style(),
6830 ),
6831 );
6832 ext_widgets::menu_list(
6833 ui,
6834 menu_columns,
6835 "menus.menu_list",
6836 &active_items,
6837 state.menu_bar.active_item,
6838 ext_widgets::MenuListOptions::default().with_action_prefix("menus.item"),
6839 );
6840 if let Some(active_item) = state.menu_bar.active_item {
6841 if let Some(children) = active_items
6842 .get(active_item)
6843 .and_then(|item| item.children())
6844 {
6845 let submenu_column = ui.add_child(
6846 menu_columns,
6847 UiNode::container(
6848 "menus.submenu_column",
6849 Layout::column()
6850 .size(LayoutSize::new(
6851 LayoutDimension::Auto,
6852 LayoutDimension::Auto,
6853 ))
6854 .gap(LayoutGap::points(0.0, 0.0))
6855 .flex(0.0, 0.0, LayoutDimension::Auto)
6856 .to_layout_style(),
6857 ),
6858 );
6859 let offset = menu_item_top_offset(&active_items, active_item);
6860 if offset > 0.0 {
6861 widgets::spacer(
6862 ui,
6863 submenu_column,
6864 "menus.submenu_spacer",
6865 LayoutStyle::new().with_width(1.0).with_height(offset),
6866 );
6867 }
6868 ext_widgets::menu_list(
6869 ui,
6870 submenu_column,
6871 "menus.submenu",
6872 children,
6873 Some(0),
6874 ext_widgets::MenuListOptions::default().with_action_prefix("menus.item"),
6875 );
6876 }
6877 }
6878 }
6879 divider(ui, body, "menus.divider.buttons");
6880 widgets::label(
6881 ui,
6882 body,
6883 "menus.buttons.title",
6884 "Menu buttons",
6885 text(12.0, color(166, 176, 190)),
6886 LayoutStyle::new().with_width_percent(1.0),
6887 );
6888 let button_row = row(ui, body, "menus.buttons", 10.0);
6889 let button_items = menu_items(state.menu_autosave);
6890 ext_widgets::menu_button(
6891 ui,
6892 button_row,
6893 "menus.menu_button",
6894 "Menu button",
6895 &button_items,
6896 &state.menu_button,
6897 None,
6898 ext_widgets::MenuButtonOptions::default().with_action("menus.menu_button"),
6899 );
6900 ext_widgets::image_text_menu_button(
6901 ui,
6902 button_row,
6903 "menus.image_text_menu_button",
6904 "Image text",
6905 icon_image(BuiltInIcon::Folder),
6906 &button_items,
6907 &state.image_text_menu_button,
6908 None,
6909 ext_widgets::MenuButtonOptions::default().with_action("menus.image_text_menu_button"),
6910 );
6911 ext_widgets::image_menu_button(
6912 ui,
6913 button_row,
6914 "menus.image_menu_button",
6915 icon_image(BuiltInIcon::Settings),
6916 &button_items,
6917 &state.image_menu_button,
6918 None,
6919 ext_widgets::MenuButtonOptions::default().with_action("menus.image_menu_button"),
6920 );
6921 if state.menu_button.open || state.image_text_menu_button.open || state.image_menu_button.open {
6922 let active = state
6923 .menu_button
6924 .navigation
6925 .active_path
6926 .first()
6927 .copied()
6928 .or_else(|| {
6929 state
6930 .image_text_menu_button
6931 .navigation
6932 .active_path
6933 .first()
6934 .copied()
6935 })
6936 .or_else(|| {
6937 state
6938 .image_menu_button
6939 .navigation
6940 .active_path
6941 .first()
6942 .copied()
6943 });
6944 ext_widgets::menu_list(
6945 ui,
6946 body,
6947 "menus.button_menu",
6948 &button_items,
6949 active,
6950 ext_widgets::MenuListOptions::default().with_action_prefix("menus.item"),
6951 );
6952 }
6953
6954 divider(ui, body, "menus.divider.context");
6955 widgets::label(
6956 ui,
6957 body,
6958 "menus.context.title",
6959 "Context menu",
6960 text(12.0, color(166, 176, 190)),
6961 LayoutStyle::new().with_width_percent(1.0),
6962 );
6963 let context_row = row(ui, body, "menus.context.controls", 8.0);
6964 button(
6965 ui,
6966 context_row,
6967 "menus.context.open",
6968 "Open context",
6969 "menus.context.open",
6970 button_visual(48, 112, 184),
6971 );
6972 button(
6973 ui,
6974 context_row,
6975 "menus.context.close",
6976 "Close",
6977 "menus.context.close",
6978 button_visual(58, 78, 96),
6979 );
6980 let mut context_options =
6981 ext_widgets::MenuListOptions::default().with_action_prefix("menus.context");
6982 context_options.width = 240.0;
6983 context_options.max_visible_rows = 6;
6984 let _ = ext_widgets::context_menu(
6985 ui,
6986 parent,
6987 "menus.context_menu",
6988 &button_items,
6989 &state.context_menu,
6990 UiRect::new(0.0, 0.0, 560.0, 460.0),
6991 ext_widgets::PopupPlacement::default(),
6992 context_options,
6993 );
6994}
6995
6996fn menu_demo_context_anchor() -> UiPoint {
6997 UiPoint::new(30.0, 390.0)
6998}
6999
7000fn command_palette(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7001 let body = section_with_min_viewport(
7002 ui,
7003 parent,
7004 "command_palette",
7005 "Command palette",
7006 UiSize::new(240.0, 72.0),
7007 );
7008 let items = command_palette_items_with_history(&state.command_history);
7009 let mut trigger_options =
7010 widgets::ButtonOptions::new(LayoutStyle::new().with_width(150.0).with_height(32.0))
7011 .with_action(if state.command_palette_open {
7012 "command_palette.close"
7013 } else {
7014 "command_palette.open"
7015 })
7016 .with_accessibility_label(if state.command_palette_open {
7017 "Close command palette"
7018 } else {
7019 "Open command palette"
7020 });
7021 trigger_options.visual = button_visual(48, 112, 184);
7022 trigger_options.hovered_visual = Some(button_visual(62, 126, 196));
7023 trigger_options.pressed_visual = Some(button_visual(38, 82, 136));
7024 trigger_options.text_style = text(13.0, color(246, 249, 252));
7025 widgets::button(
7026 ui,
7027 body,
7028 "command_palette.open",
7029 if state.command_palette_open {
7030 "Close palette"
7031 } else {
7032 "Open palette"
7033 },
7034 trigger_options,
7035 );
7036 widgets::label(
7037 ui,
7038 body,
7039 "command_palette.last",
7040 format!("Last command: {}", state.last_command),
7041 text(12.0, color(154, 166, 184)),
7042 LayoutStyle::new().with_width_percent(1.0),
7043 );
7044 if state.command_palette_open {
7045 let palette_width = command_palette_popup_width(state.last_desktop_size);
7046 let mut options =
7047 ext_widgets::CommandPaletteOptions::default().with_action_prefix("command_palette");
7048 options.width = palette_width;
7049 options.row_height = 44.0;
7050 options.max_visible_rows = 5;
7051 options.text_style = text(13.0, color(238, 244, 252));
7052 options.muted_text_style = text(12.0, color(166, 178, 196));
7053 options.z_index = SHOWCASE_WINDOW_Z_MAX.saturating_add(40);
7054 ext_widgets::command_palette(
7055 ui,
7056 body,
7057 "command_palette.panel",
7058 &items,
7059 &state.command_palette,
7060 Some(command_palette_popup(
7061 state.last_desktop_size,
7062 palette_width,
7063 )),
7064 options,
7065 );
7066 }
7067}
7068
7069fn command_palette_popup_width(desktop_size: UiSize) -> f32 {
7070 (desktop_size.width - 48.0).clamp(320.0, 560.0)
7071}
7072
7073fn command_palette_popup(desktop_size: UiSize, width: f32) -> ext_widgets::AnchoredPopup {
7074 let viewport = UiRect::new(0.0, 0.0, desktop_size.width, desktop_size.height);
7075 let x = ((desktop_size.width - width) * 0.5).max(12.0);
7076 let y = (desktop_size.height * 0.12).clamp(48.0, 96.0);
7077 ext_widgets::AnchoredPopup::new(
7078 UiRect::new(x, y, width, 0.0),
7079 viewport,
7080 ext_widgets::PopupPlacement::new(
7081 ext_widgets::PopupSide::Bottom,
7082 ext_widgets::PopupAlign::Center,
7083 )
7084 .with_offset(0.0)
7085 .with_flip(false)
7086 .with_viewport_margin(12.0),
7087 )
7088}
7089
7090#[allow(clippy::field_reassign_with_default)]
7091fn progress_indicator(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7092 let body = section(ui, parent, "progress", "Progress indicator");
7093 let animated = smooth_loop(state.progress_phase * 0.85, 0.0) * 100.0;
7094 let mut progress = ext_widgets::ProgressIndicatorOptions::default();
7095 progress.layout = LayoutStyle::new().with_width_percent(1.0).with_height(10.0);
7096 progress.accessibility_label = Some("Progress".to_string());
7097 ext_widgets::progress_indicator(
7098 ui,
7099 body,
7100 "progress.primary",
7101 ext_widgets::ProgressIndicatorValue::percent(animated),
7102 progress,
7103 );
7104 let compact_value = smooth_loop(state.progress_phase * 1.15, 0.7) * 100.0;
7105 let mut compact = ext_widgets::ProgressIndicatorOptions::default();
7106 compact.layout = LayoutStyle::new().with_width_percent(1.0).with_height(6.0);
7107 compact.fill_visual = UiVisual::panel(color(111, 203, 159), None, 3.0);
7108 ext_widgets::progress_indicator(
7109 ui,
7110 body,
7111 "progress.compact",
7112 ext_widgets::ProgressIndicatorValue::percent(compact_value),
7113 compact,
7114 );
7115 let warning_value = smooth_loop(state.progress_phase * 0.65, 1.4) * 100.0;
7116 let mut warning = ext_widgets::ProgressIndicatorOptions::default();
7117 warning.layout = LayoutStyle::new().with_width_percent(1.0).with_height(14.0);
7118 warning.fill_visual = UiVisual::panel(color(232, 186, 88), None, 4.0);
7119 ext_widgets::progress_indicator(
7120 ui,
7121 body,
7122 "progress.warning",
7123 ext_widgets::ProgressIndicatorValue::percent(warning_value),
7124 warning,
7125 );
7126 let logged_value =
7127 (state.progress_loading_elapsed / PROGRESS_LOGGED_DURATION_SECONDS * 100.0).min(100.0);
7128 let logged_entries = progress_demo_logs(logged_value);
7129 progress_loading_panel(
7130 ui,
7131 body,
7132 "progress.logged",
7133 logged_value,
7134 &logged_entries,
7135 state,
7136 );
7137 let spinner_row = row(ui, body, "progress.spinner.row", 8.0);
7138 widgets::spinner(
7139 ui,
7140 spinner_row,
7141 "progress.spinner",
7142 widgets::SpinnerOptions::default()
7143 .with_phase(state.progress_phase)
7144 .with_accessibility_label("Loading spinner"),
7145 );
7146 widgets::label(
7147 ui,
7148 spinner_row,
7149 "progress.spinner.label",
7150 "Spinner",
7151 text(12.0, color(196, 210, 230)),
7152 LayoutStyle::new().with_width_percent(1.0),
7153 );
7154}
7155
7156fn progress_loading_panel(
7157 ui: &mut UiDocument,
7158 parent: UiNodeId,
7159 name: &'static str,
7160 progress_value: f32,
7161 logs: &[ext_widgets::ProgressLogEntry],
7162 state: &ShowcaseState,
7163) {
7164 let panel = ui.add_child(
7165 parent,
7166 UiNode::container(
7167 name,
7168 LayoutStyle::column()
7169 .with_width_percent(1.0)
7170 .with_padding(10.0)
7171 .with_gap(8.0)
7172 .with_flex_shrink(0.0),
7173 )
7174 .with_visual(UiVisual::panel(
7175 color(17, 21, 27),
7176 Some(StrokeStyle::new(color(70, 82, 101), 1.0)),
7177 4.0,
7178 ))
7179 .with_accessibility(
7180 AccessibilityMeta::new(AccessibilityRole::Group).label("Loading progress with logs"),
7181 ),
7182 );
7183
7184 let progress_row = row(ui, panel, "progress.logged.progress_row", 8.0);
7185 let progress_slot = ui.add_child(
7186 progress_row,
7187 UiNode::container(
7188 "progress.logged.progress_slot",
7189 LayoutStyle::new()
7190 .with_width(0.0)
7191 .with_height(30.0)
7192 .with_flex_grow(1.0)
7193 .with_flex_shrink(1.0),
7194 ),
7195 );
7196 let mut progress = ext_widgets::ProgressIndicatorOptions::default();
7197 progress.layout = LayoutStyle::new()
7198 .with_width_percent(1.0)
7199 .with_height(10.0)
7200 .with_flex_grow(1.0)
7201 .with_flex_shrink(1.0);
7202 progress.fill_visual = UiVisual::panel(color(111, 203, 159), None, 3.0);
7203 progress.accessibility_label = Some("Logged loading progress".to_string());
7204 ext_widgets::progress_indicator(
7205 ui,
7206 progress_slot,
7207 "progress.logged.progress",
7208 ext_widgets::ProgressIndicatorValue::percent(progress_value),
7209 progress,
7210 );
7211 let mut reset = widgets::ButtonOptions::new(
7212 LayoutStyle::new()
7213 .with_width(76.0)
7214 .with_height(30.0)
7215 .with_flex_shrink(0.0),
7216 )
7217 .with_action("progress.logged.reset");
7218 reset.visual = button_visual(38, 46, 58);
7219 reset.hovered_visual = Some(button_visual(65, 86, 106));
7220 reset.pressed_visual = Some(button_visual(34, 54, 84));
7221 reset.text_style = text(12.0, color(238, 244, 252));
7222 widgets::button(ui, progress_row, "progress.logged.reset", "Reset", reset);
7223
7224 let log_scroll = progress_log_scroll_state(
7225 state.progress_logs_scroll.offset().y,
7226 logs.len(),
7227 state.progress_logs_follow_tail,
7228 );
7229 let logs_node = ui.add_child(
7230 panel,
7231 UiNode::container(
7232 "progress.logged.logs",
7233 LayoutStyle::column()
7234 .with_width_percent(1.0)
7235 .with_height(PROGRESS_LOG_VIEWPORT_HEIGHT)
7236 .with_flex_shrink(0.0),
7237 )
7238 .with_visual(UiVisual::panel(
7239 color(11, 15, 21),
7240 Some(StrokeStyle::new(color(45, 57, 73), 1.0)),
7241 3.0,
7242 ))
7243 .with_scroll(ScrollAxes::VERTICAL)
7244 .with_accessibility(
7245 AccessibilityMeta::new(AccessibilityRole::List)
7246 .label("Loading logs")
7247 .value(format!("{} entries", logs.len())),
7248 ),
7249 );
7250 {
7251 let node = ui.node_mut(logs_node);
7252 node.set_action("progress.logged.logs.scroll");
7253 node.set_scroll(log_scroll);
7254 }
7255
7256 if logs.is_empty() {
7257 ui.add_child(
7258 logs_node,
7259 UiNode::text(
7260 "progress.logged.logs.empty",
7261 "Waiting for log output...",
7262 text(12.0, color(154, 166, 184)),
7263 LayoutStyle::new()
7264 .with_width_percent(1.0)
7265 .with_height(PROGRESS_LOG_ROW_HEIGHT)
7266 .with_padding(4.0)
7267 .with_flex_shrink(0.0),
7268 )
7269 .with_accessibility(AccessibilityMeta::new(AccessibilityRole::Status).label("No logs")),
7270 );
7271 } else {
7272 for (index, entry) in logs.iter().enumerate() {
7273 let mut text_style = text(12.0, entry.level.color());
7274 text_style.line_height = 18.0;
7275 ui.add_child(
7276 logs_node,
7277 UiNode::text(
7278 format!("{name}.logs.row.{index}"),
7279 format!("[{}] {}", entry.level.as_str(), entry.message),
7280 text_style,
7281 LayoutStyle::new()
7282 .with_width_percent(1.0)
7283 .with_height(PROGRESS_LOG_ROW_HEIGHT)
7284 .with_padding(4.0)
7285 .with_flex_shrink(0.0),
7286 )
7287 .with_accessibility(
7288 AccessibilityMeta::new(AccessibilityRole::ListItem).label(format!(
7289 "{}: {}",
7290 entry.level.as_str(),
7291 entry.message
7292 )),
7293 ),
7294 );
7295 }
7296 }
7297}
7298
7299fn progress_log_scroll_state(
7300 saved_offset_y: f32,
7301 log_count: usize,
7302 follow_tail: bool,
7303) -> operad::ScrollState {
7304 let content_height = log_count.max(1) as f32 * PROGRESS_LOG_ROW_HEIGHT;
7305 let max_offset = (content_height - PROGRESS_LOG_VIEWPORT_HEIGHT).max(0.0);
7306 let offset_y = if follow_tail {
7307 max_offset
7308 } else {
7309 saved_offset_y.min(max_offset)
7310 };
7311 operad::ScrollState::new(ScrollAxes::VERTICAL)
7312 .with_sizes(
7313 UiSize::new(8.0, PROGRESS_LOG_VIEWPORT_HEIGHT),
7314 UiSize::new(8.0, content_height),
7315 )
7316 .with_offset(UiPoint::new(0.0, offset_y))
7317}
7318
7319fn progress_demo_logs(progress: f32) -> Vec<ext_widgets::ProgressLogEntry> {
7320 let mut logs = vec![
7321 ext_widgets::ProgressLogEntry::info("Initializing renderer"),
7322 ext_widgets::ProgressLogEntry::info("Mounting content archive"),
7323 ];
7324 if progress >= 24.0 {
7325 logs.push(ext_widgets::ProgressLogEntry::success(
7326 "Compiled material shaders",
7327 ));
7328 }
7329 if progress >= 48.0 {
7330 logs.push(ext_widgets::ProgressLogEntry::info("Decoded texture atlas"));
7331 }
7332 if progress >= 72.0 {
7333 logs.push(ext_widgets::ProgressLogEntry::warning(
7334 "Optional cloud profile is still pending",
7335 ));
7336 }
7337 if progress >= 96.0 {
7338 logs.push(ext_widgets::ProgressLogEntry::success("Ready"));
7339 }
7340 logs
7341}
7342
7343#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7344enum EaseCurveKind {
7345 Quad,
7346 Cubic,
7347 Quart,
7348 Expo,
7349 Back,
7350 Elastic,
7351 Bounce,
7352}
7353
7354impl EaseCurveKind {
7355 fn id(self) -> &'static str {
7356 match self {
7357 Self::Quad => "quad",
7358 Self::Cubic => "cubic",
7359 Self::Quart => "quart",
7360 Self::Expo => "expo",
7361 Self::Back => "back",
7362 Self::Elastic => "elastic",
7363 Self::Bounce => "bounce",
7364 }
7365 }
7366
7367 fn base_label(self) -> &'static str {
7368 match self {
7369 Self::Quad => "quad",
7370 Self::Cubic => "cubic",
7371 Self::Quart => "quart",
7372 Self::Expo => "expo",
7373 Self::Back => "back",
7374 Self::Elastic => "elastic",
7375 Self::Bounce => "bounce",
7376 }
7377 }
7378
7379 fn sample_out(self, progress: f32) -> f32 {
7380 let t = unit(progress);
7381 match self {
7382 Self::Quad => 1.0 - (1.0 - t).powi(2),
7383 Self::Cubic => 1.0 - (1.0 - t).powi(3),
7384 Self::Quart => 1.0 - (1.0 - t).powi(4),
7385 Self::Expo => {
7386 if t >= 1.0 {
7387 1.0
7388 } else {
7389 1.0 - 2.0_f32.powf(-10.0 * t)
7390 }
7391 }
7392 Self::Back => {
7393 let c1 = 1.70158;
7394 let c3 = c1 + 1.0;
7395 1.0 + c3 * (t - 1.0).powi(3) + c1 * (t - 1.0).powi(2)
7396 }
7397 Self::Elastic => {
7398 if t <= 0.0 {
7399 0.0
7400 } else if t >= 1.0 {
7401 1.0
7402 } else {
7403 let period = (2.0 * std::f32::consts::PI) / 3.0;
7404 2.0_f32.powf(-10.0 * t) * ((t * 10.0 - 0.75) * period).sin() + 1.0
7405 }
7406 }
7407 Self::Bounce => ease_out_bounce(t),
7408 }
7409 }
7410}
7411
7412#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7413enum EaseDirection {
7414 In,
7415 Out,
7416}
7417
7418impl EaseDirection {
7419 fn label_prefix(self) -> &'static str {
7420 match self {
7421 Self::In => "Ease in",
7422 Self::Out => "Ease out",
7423 }
7424 }
7425}
7426
7427#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7428struct EasingFunction {
7429 direction: EaseDirection,
7430 kind: EaseCurveKind,
7431}
7432
7433impl EasingFunction {
7434 const fn new(direction: EaseDirection, kind: EaseCurveKind) -> Self {
7435 Self { direction, kind }
7436 }
7437
7438 fn label(self) -> String {
7439 format!(
7440 "{} {}",
7441 self.direction.label_prefix(),
7442 self.kind.base_label()
7443 )
7444 }
7445
7446 fn sample(self, progress: f32) -> f32 {
7447 let t = unit(progress);
7448 match self.direction {
7449 EaseDirection::In => 1.0 - self.kind.sample_out(1.0 - t),
7450 EaseDirection::Out => self.kind.sample_out(t),
7451 }
7452 }
7453}
7454
7455fn ease_out_bounce(t: f32) -> f32 {
7456 let n1 = 7.5625;
7457 let d1 = 2.75;
7458 if t < 1.0 / d1 {
7459 n1 * t * t
7460 } else if t < 2.0 / d1 {
7461 let t = t - 1.5 / d1;
7462 n1 * t * t + 0.75
7463 } else if t < 2.5 / d1 {
7464 let t = t - 2.25 / d1;
7465 n1 * t * t + 0.9375
7466 } else {
7467 let t = t - 2.625 / d1;
7468 n1 * t * t + 0.984375
7469 }
7470}
7471
7472fn easing_options(direction: EaseDirection) -> Vec<ext_widgets::SelectOption> {
7473 [
7474 EaseCurveKind::Quad,
7475 EaseCurveKind::Cubic,
7476 EaseCurveKind::Quart,
7477 EaseCurveKind::Expo,
7478 EaseCurveKind::Back,
7479 EaseCurveKind::Elastic,
7480 EaseCurveKind::Bounce,
7481 ]
7482 .into_iter()
7483 .map(|kind| {
7484 ext_widgets::SelectOption::new(kind.id(), EasingFunction::new(direction, kind).label())
7485 })
7486 .collect()
7487}
7488
7489fn selected_easing(
7490 state: &ext_widgets::SelectMenuState,
7491 direction: EaseDirection,
7492) -> EasingFunction {
7493 let options = easing_options(direction);
7494 let kind = match state.selected_id(&options) {
7495 Some("quad") => EaseCurveKind::Quad,
7496 Some("quart") => EaseCurveKind::Quart,
7497 Some("expo") => EaseCurveKind::Expo,
7498 Some("back") => EaseCurveKind::Back,
7499 Some("elastic") => EaseCurveKind::Elastic,
7500 Some("bounce") => EaseCurveKind::Bounce,
7501 _ => EaseCurveKind::Cubic,
7502 };
7503 EasingFunction::new(direction, kind)
7504}
7505
7506fn animation_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7507 let body = section(ui, parent, "animation", "Animation");
7508
7509 if let Some(section) = animation_section(
7510 ui,
7511 body,
7512 "animation.timed",
7513 "Timed playback",
7514 state.animation_timed_expanded,
7515 ) {
7516 let live_stage = animation_stage(ui, section, "animation.live.stage");
7517 let live_amount = smooth_loop(state.progress_phase * 1.65, 0.0);
7518 let live_values = animation_blend_machine(
7519 ANIMATION_INPUT_PROGRESS,
7520 live_amount,
7521 UiPoint::new(220.0, 0.0),
7522 0.88,
7523 1.10,
7524 1.0,
7525 )
7526 .with_bool_input("looping", true)
7527 .values();
7528 ui.add_child(
7529 live_stage,
7530 UiNode::scene(
7531 "animation.live.orb",
7532 animation_orb_primitives(
7533 color(108, 180, 255),
7534 ANIMATION_ORB_SIZE * live_values.scale,
7535 UiPoint::new(
7536 28.0 + live_values.translate.x,
7537 37.0 + live_values.translate.y,
7538 ),
7539 ),
7540 animation_scene_layout(),
7541 )
7542 .with_accessibility(
7543 AccessibilityMeta::new(AccessibilityRole::Image).label("Looping orb"),
7544 ),
7545 );
7546 }
7547
7548 if let Some(section) = animation_section(
7549 ui,
7550 body,
7551 "animation.scrub",
7552 "Scrubbed input",
7553 state.animation_scrub_expanded,
7554 ) {
7555 let scrub_row = row(ui, section, "animation.scrub.row", 10.0);
7556 widgets::slider(
7557 ui,
7558 scrub_row,
7559 "animation.scrub.slider",
7560 state.animation_scrub,
7561 0.0..1.0,
7562 widgets::SliderOptions::default()
7563 .with_layout(
7564 LayoutStyle::new()
7565 .with_width(200.0)
7566 .with_height(28.0)
7567 .with_flex_shrink(0.0),
7568 )
7569 .with_value_edit_action("animation.scrub"),
7570 );
7571 widgets::label(
7572 ui,
7573 scrub_row,
7574 "animation.scrub.value",
7575 format!("{:.0}%", state.animation_scrub * 100.0),
7576 text(12.0, color(186, 198, 216)),
7577 LayoutStyle::new().with_width_percent(1.0),
7578 );
7579 let scrub_stage = animation_stage(ui, section, "animation.scrub.stage");
7580 let scrub_values = animation_blend_machine(
7581 ANIMATION_INPUT_SCRUB,
7582 state.animation_scrub,
7583 UiPoint::new(220.0, 0.0),
7584 0.82,
7585 1.14,
7586 1.0,
7587 )
7588 .values();
7589 ui.add_child(
7590 scrub_stage,
7591 UiNode::scene(
7592 "animation.scrub.shape",
7593 animation_morph_shape_primitives(
7594 color(111, 203, 159),
7595 ANIMATION_SHAPE_SIZE * scrub_values.scale,
7596 UiPoint::new(
7597 28.0 + scrub_values.translate.x,
7598 37.0 + scrub_values.translate.y,
7599 ),
7600 scrub_values.morph,
7601 ),
7602 animation_scene_layout(),
7603 )
7604 .with_accessibility(
7605 AccessibilityMeta::new(AccessibilityRole::Image).label("Scrubbed morphing shape"),
7606 ),
7607 );
7608 }
7609
7610 if let Some(section) = animation_section(
7611 ui,
7612 body,
7613 "animation.state",
7614 "Boolean input transition",
7615 state.animation_state_expanded,
7616 ) {
7617 let state_row = row(ui, section, "animation.state.row", 10.0);
7618 let mut open = widgets::ButtonOptions::new(
7619 LayoutStyle::new()
7620 .with_width(92.0)
7621 .with_height(30.0)
7622 .with_flex_shrink(0.0),
7623 )
7624 .with_action("animation.open");
7625 open.visual = if state.animation_open {
7626 button_visual(48, 112, 184)
7627 } else {
7628 button_visual(38, 46, 58)
7629 };
7630 open.hovered_visual = Some(button_visual(65, 86, 106));
7631 open.pressed_visual = Some(button_visual(34, 54, 84));
7632 open.text_style = text(12.0, color(238, 244, 252));
7633 widgets::button(
7634 ui,
7635 state_row,
7636 "animation.open",
7637 if state.animation_open {
7638 "Close"
7639 } else {
7640 "Open"
7641 },
7642 open,
7643 );
7644 let open_stage = animation_stage(ui, section, "animation.state.stage");
7645 let panel_offset = if state.animation_open {
7646 UiPoint::new(
7647 ANIMATION_STAGE_MIN_WIDTH - ANIMATION_PANEL_WIDTH - ANIMATION_PANEL_INSET_X,
7648 ANIMATION_PANEL_Y,
7649 )
7650 } else {
7651 UiPoint::new(ANIMATION_PANEL_INSET_X, ANIMATION_PANEL_Y)
7652 };
7653 ui.add_child(
7654 open_stage,
7655 UiNode::scene(
7656 "animation.state.panel",
7657 animation_panel_primitives(panel_offset),
7658 animation_scene_layout(),
7659 )
7660 .with_animation(animation_open_machine(state.animation_open))
7661 .with_accessibility(
7662 AccessibilityMeta::new(AccessibilityRole::Image).label("Open state panel"),
7663 ),
7664 );
7665 }
7666
7667 if let Some(section) = animation_section(
7668 ui,
7669 body,
7670 "animation.interaction",
7671 "Interaction inputs",
7672 state.animation_interaction_expanded,
7673 ) {
7674 let interaction_stage = animation_stage(ui, section, "animation.interaction.stage");
7675 ui.add_child(
7676 interaction_stage,
7677 UiNode::scene(
7678 "animation.interaction.target",
7679 animation_interaction_primitives(
7680 color(176, 126, 230),
7681 ANIMATION_ORB_SIZE,
7682 UiPoint::new(40.0, 37.0),
7683 ),
7684 animation_scene_layout(),
7685 )
7686 .with_input(InputBehavior::BUTTON)
7687 .with_animation(animation_interaction_machine())
7688 .with_accessibility(
7689 AccessibilityMeta::new(AccessibilityRole::Button)
7690 .label("Interaction animation target")
7691 .focusable(),
7692 ),
7693 );
7694 }
7695}
7696
7697fn easing_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7698 let body = section(ui, parent, "easing", "Easing");
7699 let linear_progress = (state.progress_phase * 0.25).rem_euclid(1.0);
7700 easing_curve_demo(
7701 ui,
7702 body,
7703 "easing.in",
7704 "Ease-in functions",
7705 EaseDirection::In,
7706 &state.easing_in,
7707 linear_progress,
7708 );
7709 divider(ui, body, "easing.divider");
7710 easing_curve_demo(
7711 ui,
7712 body,
7713 "easing.out",
7714 "Ease-out functions",
7715 EaseDirection::Out,
7716 &state.easing_out,
7717 linear_progress,
7718 );
7719}
7720
7721fn easing_curve_demo(
7722 ui: &mut UiDocument,
7723 parent: UiNodeId,
7724 name: &'static str,
7725 title: &'static str,
7726 direction: EaseDirection,
7727 state: &ext_widgets::SelectMenuState,
7728 linear_progress: f32,
7729) {
7730 widgets::label(
7731 ui,
7732 parent,
7733 format!("{name}.title"),
7734 title,
7735 text(12.0, color(186, 198, 216)),
7736 LayoutStyle::new().with_width_percent(1.0),
7737 );
7738
7739 let options = easing_options(direction);
7740 let selected = selected_easing(state, direction);
7741 let eased_progress = selected.sample(linear_progress);
7742 let controls = row(ui, parent, format!("{name}.controls"), 10.0);
7743 let dropdown_width = 184.0;
7744 let dropdown_name = format!("{name}.dropdown");
7745 let dropdown_anchor = ui.add_child(
7746 controls,
7747 UiNode::container(
7748 format!("{name}.dropdown.anchor"),
7749 LayoutStyle::new()
7750 .with_width(dropdown_width)
7751 .with_height(30.0)
7752 .with_flex_shrink(0.0),
7753 ),
7754 );
7755 let dropdown_nodes = ext_widgets::dropdown_select(
7756 ui,
7757 dropdown_anchor,
7758 dropdown_name.clone(),
7759 &options,
7760 state,
7761 Some(select_popup(
7762 UiRect::new(0.0, 0.0, dropdown_width, 30.0),
7763 UiRect::new(0.0, 0.0, EASING_STAGE_MIN_WIDTH, 260.0),
7764 )),
7765 dropdown_select_options(
7766 dropdown_width,
7767 dropdown_name.as_str(),
7768 "Ease function",
7769 title,
7770 ),
7771 );
7772 ui.node_mut(dropdown_nodes.trigger)
7773 .set_action(format!("{name}.dropdown.toggle"));
7774 widgets::label(
7775 ui,
7776 controls,
7777 format!("{name}.value"),
7778 format!(
7779 "{:.0}% -> {:.0}%",
7780 linear_progress * 100.0,
7781 eased_progress * 100.0
7782 ),
7783 text(12.0, color(186, 198, 216)),
7784 LayoutStyle::new().with_width_percent(1.0),
7785 );
7786
7787 let stage = easing_stage(ui, parent, format!("{name}.stage"));
7788 ui.add_child(
7789 stage,
7790 UiNode::scene(
7791 format!("{name}.graph"),
7792 easing_curve_primitives(selected, linear_progress),
7793 animation_scene_layout(),
7794 )
7795 .with_accessibility(
7796 AccessibilityMeta::new(AccessibilityRole::Image)
7797 .label(format!("{} curve and looping marker", selected.label())),
7798 ),
7799 );
7800}
7801
7802fn animation_section(
7803 ui: &mut UiDocument,
7804 parent: UiNodeId,
7805 name: &'static str,
7806 title: &'static str,
7807 expanded: bool,
7808) -> Option<UiNodeId> {
7809 let mut options = widgets::CollapsingHeaderOptions::default()
7810 .expanded(expanded)
7811 .with_toggle_action(format!("{name}.toggle"));
7812 options.text_style = text(12.0, color(220, 228, 238));
7813 options.indicator_text_style = text(12.0, color(186, 198, 216));
7814 options.root_visual = UiVisual::panel(
7815 color(17, 22, 29),
7816 Some(StrokeStyle::new(color(48, 58, 72), 1.0)),
7817 6.0,
7818 );
7819 options.header_visual = UiVisual::panel(color(21, 26, 33), None, 0.0);
7820 options.hovered_visual = UiVisual::panel(color(38, 48, 61), None, 0.0);
7821 options.pressed_visual = UiVisual::panel(color(27, 36, 48), None, 0.0);
7822 options.body_layout = LayoutStyle::column()
7823 .with_width_percent(1.0)
7824 .with_padding(10.0)
7825 .with_gap(10.0);
7826 widgets::collapsing_header(ui, parent, name, title, options).body
7827}
7828
7829fn animation_stage(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>) -> UiNodeId {
7830 let layout = LayoutStyle::row()
7831 .with_width_percent(1.0)
7832 .with_height(ANIMATION_STAGE_HEIGHT)
7833 .with_align_items(taffy::prelude::AlignItems::Center)
7834 .with_flex_shrink(0.0);
7835 let layout = operad::layout::with_min_size(
7836 layout,
7837 operad::length(ANIMATION_STAGE_MIN_WIDTH),
7838 operad::length(ANIMATION_STAGE_HEIGHT),
7839 );
7840 ui.add_child(
7841 parent,
7842 UiNode::container(name, layout).with_visual(UiVisual::panel(
7843 color(16, 21, 28),
7844 Some(StrokeStyle::new(color(48, 58, 72), 1.0)),
7845 6.0,
7846 )),
7847 )
7848}
7849
7850fn easing_stage(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>) -> UiNodeId {
7851 let layout = LayoutStyle::row()
7852 .with_width_percent(1.0)
7853 .with_height(EASING_STAGE_HEIGHT)
7854 .with_align_items(taffy::prelude::AlignItems::Center)
7855 .with_flex_shrink(0.0);
7856 let layout = operad::layout::with_min_size(
7857 layout,
7858 operad::length(EASING_STAGE_MIN_WIDTH),
7859 operad::length(EASING_STAGE_HEIGHT),
7860 );
7861 ui.add_child(
7862 parent,
7863 UiNode::container(name, layout).with_visual(UiVisual::panel(
7864 color(16, 21, 28),
7865 Some(StrokeStyle::new(color(48, 58, 72), 1.0)),
7866 6.0,
7867 )),
7868 )
7869}
7870
7871fn animation_scene_layout() -> LayoutStyle {
7872 let layout = LayoutStyle::new()
7873 .with_width_percent(1.0)
7874 .with_height_percent(1.0)
7875 .with_flex_grow(1.0)
7876 .with_flex_shrink(1.0);
7877 operad::layout::with_min_size(layout, operad::length(0.0), operad::length(0.0))
7878}
7879
7880fn easing_curve_primitives(function: EasingFunction, linear_progress: f32) -> Vec<ScenePrimitive> {
7881 let mut primitives = Vec::new();
7882 let graph = UiRect::new(24.0, 24.0, 172.0, 112.0);
7883 primitives.push(ScenePrimitive::Rect(
7884 PaintRect::solid(graph, color(12, 17, 24))
7885 .stroke(AlignedStroke::inside(StrokeStyle::new(
7886 color(58, 70, 88),
7887 1.0,
7888 )))
7889 .corner_radii(CornerRadii::uniform(4.0)),
7890 ));
7891 for index in 1..4 {
7892 let fraction = index as f32 / 4.0;
7893 let x = graph.x + graph.width * fraction;
7894 let y = graph.y + graph.height * fraction;
7895 primitives.push(ScenePrimitive::Line {
7896 from: UiPoint::new(x, graph.y),
7897 to: UiPoint::new(x, graph.y + graph.height),
7898 stroke: StrokeStyle::new(color(29, 38, 50), 1.0),
7899 });
7900 primitives.push(ScenePrimitive::Line {
7901 from: UiPoint::new(graph.x, y),
7902 to: UiPoint::new(graph.x + graph.width, y),
7903 stroke: StrokeStyle::new(color(29, 38, 50), 1.0),
7904 });
7905 }
7906 primitives.push(ScenePrimitive::Line {
7907 from: UiPoint::new(graph.x, graph.y + graph.height),
7908 to: UiPoint::new(graph.x + graph.width, graph.y + graph.height),
7909 stroke: StrokeStyle::new(color(118, 136, 162), 1.0),
7910 });
7911 primitives.push(ScenePrimitive::Line {
7912 from: UiPoint::new(graph.x, graph.y),
7913 to: UiPoint::new(graph.x, graph.y + graph.height),
7914 stroke: StrokeStyle::new(color(118, 136, 162), 1.0),
7915 });
7916
7917 let samples = 40;
7918 let mut previous = None;
7919 for index in 0..=samples {
7920 let t = index as f32 / samples as f32;
7921 let eased = function.sample(t);
7922 let point = UiPoint::new(
7923 graph.x + graph.width * t,
7924 graph.y + graph.height - graph.height * eased.clamp(-0.16, 1.16),
7925 );
7926 if let Some(from) = previous {
7927 primitives.push(ScenePrimitive::Line {
7928 from,
7929 to: point,
7930 stroke: StrokeStyle::new(color(112, 181, 255), 2.0),
7931 });
7932 }
7933 previous = Some(point);
7934 }
7935
7936 let eased_progress = function.sample(linear_progress);
7937 let graph_marker = UiPoint::new(
7938 graph.x + graph.width * linear_progress,
7939 graph.y + graph.height - graph.height * eased_progress.clamp(-0.16, 1.16),
7940 );
7941 primitives.push(ScenePrimitive::Circle {
7942 center: graph_marker,
7943 radius: 5.5,
7944 fill: color(248, 252, 255),
7945 stroke: Some(StrokeStyle::new(color(112, 181, 255), 2.0)),
7946 });
7947
7948 let track = UiRect::new(232.0, 64.0, 96.0, 12.0);
7949 let marker_progress = eased_progress.clamp(-0.10, 1.10);
7950 primitives.push(ScenePrimitive::Rect(
7951 PaintRect::solid(track, color(37, 46, 58)).corner_radii(CornerRadii::uniform(6.0)),
7952 ));
7953 primitives.push(ScenePrimitive::Rect(
7954 PaintRect::solid(
7955 UiRect::new(
7956 track.x,
7957 track.y,
7958 track.width * eased_progress.clamp(0.0, 1.0),
7959 track.height,
7960 ),
7961 color(108, 180, 255),
7962 )
7963 .corner_radii(CornerRadii::uniform(6.0)),
7964 ));
7965 primitives.push(ScenePrimitive::Circle {
7966 center: UiPoint::new(
7967 track.x + track.width * marker_progress,
7968 track.y + track.height * 0.5,
7969 ),
7970 radius: 14.0,
7971 fill: color(112, 181, 255),
7972 stroke: Some(StrokeStyle::new(color(232, 242, 255), 2.0)),
7973 });
7974 primitives.push(ScenePrimitive::Text(
7975 PaintText::new(
7976 function.label(),
7977 UiRect::new(222.0, 98.0, 120.0, 20.0),
7978 text(10.0, color(186, 198, 216)),
7979 )
7980 .horizontal_align(TextHorizontalAlign::Center)
7981 .multiline(false),
7982 ));
7983 primitives
7984}
7985
7986fn animation_blend_machine(
7987 input: &'static str,
7988 value: f32,
7989 translate: UiPoint,
7990 start_scale: f32,
7991 end_scale: f32,
7992 end_opacity: f32,
7993) -> AnimationMachine {
7994 let start_values = AnimatedValues::new(0.45, UiPoint::new(0.0, 0.0), start_scale);
7995 let end_values = AnimatedValues::new(end_opacity, translate, end_scale).with_morph(1.0);
7996 AnimationMachine::new(
7997 vec![
7998 AnimationState::new("start", start_values),
7999 AnimationState::new("end", end_values),
8000 ],
8001 Vec::new(),
8002 "start",
8003 )
8004 .unwrap_or_else(|_| AnimationMachine::single_state("start", start_values))
8005 .with_number_input(input, value)
8006 .with_blend_binding(AnimationBlendBinding::new(input, "start", "end"))
8007}
8008
8009fn animation_open_machine(open: bool) -> AnimationMachine {
8010 let closed_values = AnimatedValues::new(0.35, UiPoint::new(0.0, 0.0), 1.0);
8011 let open_values = AnimatedValues::new(1.0, UiPoint::new(0.0, 0.0), 1.0);
8012 let fallback_values = if open { open_values } else { closed_values };
8013 AnimationMachine::new(
8014 vec![
8015 AnimationState::new("closed", closed_values),
8016 AnimationState::new("open", open_values),
8017 ],
8018 vec![
8019 AnimationTransition::when(
8020 "closed",
8021 "open",
8022 AnimationCondition::bool(ANIMATION_INPUT_OPEN, true),
8023 0.18,
8024 ),
8025 AnimationTransition::when(
8026 "open",
8027 "closed",
8028 AnimationCondition::bool(ANIMATION_INPUT_OPEN, false),
8029 0.14,
8030 ),
8031 ],
8032 "closed",
8033 )
8034 .unwrap_or_else(|_| AnimationMachine::single_state("closed", fallback_values))
8035 .with_bool_input(ANIMATION_INPUT_OPEN, open)
8036}
8037
8038fn animation_interaction_machine() -> AnimationMachine {
8039 let rest_values = AnimatedValues::new(0.72, UiPoint::new(0.0, 0.0), 1.0);
8040 let right_values = AnimatedValues::new(1.0, UiPoint::new(0.0, 0.0), 1.0).with_morph(1.0);
8041 AnimationMachine::new(
8042 vec![
8043 AnimationState::new("rest", rest_values),
8044 AnimationState::new("right", right_values),
8045 ],
8046 Vec::new(),
8047 "rest",
8048 )
8049 .unwrap_or_else(|_| AnimationMachine::single_state("rest", rest_values))
8050 .with_number_input(ANIMATION_INPUT_POINTER_NORM_X, 0.0)
8051 .with_blend_binding(AnimationBlendBinding::new(
8052 ANIMATION_INPUT_POINTER_NORM_X,
8053 "rest",
8054 "right",
8055 ))
8056}
8057
8058fn animation_interaction_primitives(
8059 fill: ColorRgba,
8060 size: f32,
8061 offset: UiPoint,
8062) -> Vec<ScenePrimitive> {
8063 vec![
8064 ScenePrimitive::MorphPolygon {
8065 from_points: animation_square_points(size, offset),
8066 to_points: animation_pentagon_points(size, offset),
8067 amount: 0.0,
8068 fill,
8069 stroke: Some(StrokeStyle::new(color(236, 244, 255), 1.0)),
8070 },
8071 ScenePrimitive::Circle {
8072 center: UiPoint::new(offset.x + size * 0.34, offset.y + size * 0.30),
8073 radius: size * 0.10,
8074 fill: color(244, 248, 255),
8075 stroke: None,
8076 },
8077 ]
8078}
8079
8080fn animation_orb_primitives(fill: ColorRgba, size: f32, offset: UiPoint) -> Vec<ScenePrimitive> {
8081 let center = size * 0.5;
8082 let radius = size * 0.44;
8083 vec![
8084 ScenePrimitive::Circle {
8085 center: UiPoint::new(offset.x + center, offset.y + center),
8086 radius,
8087 fill,
8088 stroke: Some(StrokeStyle::new(color(236, 244, 255), 1.0)),
8089 },
8090 ScenePrimitive::Circle {
8091 center: UiPoint::new(offset.x + size * 0.34, offset.y + size * 0.30),
8092 radius: size * 0.12,
8093 fill: color(244, 248, 255),
8094 stroke: None,
8095 },
8096 ]
8097}
8098
8099fn animation_morph_shape_primitives(
8100 fill: ColorRgba,
8101 size: f32,
8102 offset: UiPoint,
8103 amount: f32,
8104) -> Vec<ScenePrimitive> {
8105 vec![ScenePrimitive::MorphPolygon {
8106 from_points: animation_square_points(size, offset),
8107 to_points: animation_pentagon_points(size, offset),
8108 amount,
8109 fill,
8110 stroke: Some(StrokeStyle::new(color(226, 246, 236), 1.0)),
8111 }]
8112}
8113
8114fn animation_square_points(size: f32, offset: UiPoint) -> Vec<UiPoint> {
8115 let inset = size * 0.08;
8116 let left = offset.x + inset;
8117 let top = offset.y + inset;
8118 let right = offset.x + size - inset;
8119 let bottom = offset.y + size - inset;
8120 let center_x = offset.x + size * 0.5;
8121 vec![
8122 UiPoint::new(center_x, top),
8123 UiPoint::new(right, top),
8124 UiPoint::new(right, bottom),
8125 UiPoint::new(left, bottom),
8126 UiPoint::new(left, top),
8127 ]
8128}
8129
8130fn animation_pentagon_points(size: f32, offset: UiPoint) -> Vec<UiPoint> {
8131 let center = size * 0.5;
8132 let radius = size * 0.46;
8133 (0..5)
8134 .map(|index| {
8135 let angle = -std::f32::consts::FRAC_PI_2 + index as f32 * std::f32::consts::TAU / 5.0;
8136 UiPoint::new(
8137 offset.x + center + angle.cos() * radius,
8138 offset.y + center + angle.sin() * radius,
8139 )
8140 })
8141 .collect()
8142}
8143
8144fn animation_panel_primitives(offset: UiPoint) -> Vec<ScenePrimitive> {
8145 vec![ScenePrimitive::Rect(
8146 PaintRect::solid(
8147 UiRect::new(
8148 offset.x,
8149 offset.y,
8150 ANIMATION_PANEL_WIDTH,
8151 ANIMATION_PANEL_HEIGHT,
8152 ),
8153 color(232, 186, 88),
8154 )
8155 .stroke(AlignedStroke::inside(StrokeStyle::new(
8156 color(255, 226, 154),
8157 1.0,
8158 )))
8159 .corner_radii(CornerRadii::uniform(6.0)),
8160 )]
8161}
8162
8163fn list_and_table_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
8164 let body = section_with_min_viewport(
8165 ui,
8166 parent,
8167 "lists_tables",
8168 "Lists and tables",
8169 UiSize::new(520.0, 0.0),
8170 );
8171
8172 let list_row = ui.add_child(
8173 body,
8174 UiNode::container(
8175 "lists_tables.list_row",
8176 Layout::row()
8177 .size(LayoutSize::new(
8178 LayoutDimension::percent(1.0),
8179 LayoutDimension::Auto,
8180 ))
8181 .gap(LayoutGap::points(10.0, 10.0))
8182 .flex_wrap(LayoutFlexWrap::Wrap)
8183 .to_layout_style(),
8184 ),
8185 );
8186 let scroll_column = ui.add_child(
8187 list_row,
8188 UiNode::container(
8189 "lists_tables.scroll_area.column",
8190 Layout::column()
8191 .min_size(LayoutSize::points(220.0, 0.0))
8192 .gap(LayoutGap::points(6.0, 6.0))
8193 .flex(1.0, 1.0, LayoutDimension::points(245.0))
8194 .to_layout_style(),
8195 ),
8196 );
8197 widgets::label(
8198 ui,
8199 scroll_column,
8200 "lists_tables.scroll_area.title",
8201 "Scrollable list",
8202 text(12.0, color(166, 176, 190)),
8203 LayoutStyle::new().with_width_percent(1.0),
8204 );
8205 let nested_scroll = widgets::scroll_area(
8206 ui,
8207 scroll_column,
8208 "lists_tables.scroll_area",
8209 ScrollAxes::VERTICAL,
8210 LayoutStyle::column()
8211 .with_width_percent(1.0)
8212 .with_height(104.0),
8213 );
8214 ui.node_mut(nested_scroll)
8215 .set_action("lists_tables.scroll_area.scroll");
8216 if let Some(scroll) = ui.node_mut(nested_scroll).scroll_mut() {
8217 scroll.set_offset(UiPoint::new(0.0, state.list_scroll));
8218 }
8219 for index in 0..6 {
8220 widgets::label(
8221 ui,
8222 nested_scroll,
8223 format!("lists_tables.scroll_area.row.{index}"),
8224 format!("Scroll row {}", index + 1),
8225 text(12.0, color(200, 212, 228)),
8226 LayoutStyle::new()
8227 .with_width_percent(1.0)
8228 .with_height(26.0)
8229 .with_flex_shrink(0.0),
8230 );
8231 }
8232
8233 let virtual_list_column = ui.add_child(
8234 list_row,
8235 UiNode::container(
8236 "lists_tables.virtual_list.column",
8237 Layout::column()
8238 .min_size(LayoutSize::points(220.0, 0.0))
8239 .gap(LayoutGap::points(6.0, 6.0))
8240 .flex(1.0, 1.0, LayoutDimension::points(245.0))
8241 .to_layout_style(),
8242 ),
8243 );
8244
8245 widgets::label(
8246 ui,
8247 virtual_list_column,
8248 "lists_tables.virtual_list.title",
8249 "Virtualized list",
8250 text(12.0, color(166, 176, 190)),
8251 LayoutStyle::new().with_width_percent(1.0),
8252 );
8253 let virtual_list = widgets::virtual_list(
8254 ui,
8255 virtual_list_column,
8256 "lists_tables.virtual_list",
8257 widgets::VirtualListSpec {
8258 row_count: 24,
8259 row_height: 28.0,
8260 viewport_height: 104.0,
8261 scroll_offset: state.virtual_scroll,
8262 overscan: 1,
8263 },
8264 |ui, row_parent, row| {
8265 widgets::label(
8266 ui,
8267 row_parent,
8268 format!("lists_tables.virtual_list.row.{row}"),
8269 format!("Virtual row {}", row + 1),
8270 text(12.0, color(214, 224, 238)),
8271 LayoutStyle::new()
8272 .with_width_percent(1.0)
8273 .with_height(28.0)
8274 .with_flex_shrink(0.0),
8275 );
8276 },
8277 );
8278 ui.node_mut(virtual_list)
8279 .set_action("lists_tables.virtual_list.scroll");
8280
8281 widgets::separator(
8282 ui,
8283 body,
8284 "lists_tables.virtualized_table.separator",
8285 widgets::SeparatorOptions::default(),
8286 );
8287 widgets::label(
8288 ui,
8289 body,
8290 "lists_tables.data_table.title",
8291 "Virtualized selectable table",
8292 text(12.0, color(166, 176, 190)),
8293 LayoutStyle::new().with_width_percent(1.0),
8294 );
8295 let virtual_controls = wrapping_row(ui, body, "lists_tables.virtualized_table.controls", 8.0);
8296 button(
8297 ui,
8298 virtual_controls,
8299 "lists_tables.virtualized_table.sort.name",
8300 if state.virtual_table_descending {
8301 "Name desc"
8302 } else {
8303 "Name asc"
8304 },
8305 "lists_tables.virtualized_table.sort.name",
8306 button_visual(38, 52, 70),
8307 );
8308 button(
8309 ui,
8310 virtual_controls,
8311 "lists_tables.virtualized_table.filter.status",
8312 if state.virtual_table_ready_only {
8313 "Ready only"
8314 } else {
8315 "All status"
8316 },
8317 "lists_tables.virtualized_table.filter.status",
8318 button_visual(38, 52, 70),
8319 );
8320 button(
8321 ui,
8322 virtual_controls,
8323 "lists_tables.virtualized_table.resize.reset",
8324 "Reset width",
8325 "lists_tables.virtualized_table.resize.reset",
8326 button_visual(38, 52, 70),
8327 );
8328
8329 let columns = virtual_table_columns(state);
8330 let visible_rows = virtual_table_visible_rows(state);
8331 let mut table_options = ext_widgets::DataTableOptions::default()
8332 .with_row_action_prefix("lists_tables.virtualized_table")
8333 .with_cell_action_prefix("lists_tables.virtualized_table")
8334 .with_scroll_action("lists_tables.virtualized_table.scroll");
8335 table_options.layout = LayoutStyle::column()
8336 .with_width_percent(1.0)
8337 .with_flex_shrink(0.0);
8338 table_options.header_visual = UiVisual::panel(
8339 color(34, 41, 50),
8340 Some(StrokeStyle::new(color(67, 78, 95), 1.0)),
8341 0.0,
8342 );
8343 table_options.header_text_style = text(12.0, color(222, 230, 240));
8344 table_options.selection = state.table_selection.clone();
8345 ext_widgets::virtualized_data_table(
8346 ui,
8347 body,
8348 "lists_tables.virtualized_table",
8349 &columns,
8350 ext_widgets::VirtualDataTableSpec {
8351 row_count: visible_rows.len(),
8352 row_height: 28.0,
8353 viewport_width: 520.0,
8354 viewport_height: 156.0,
8355 scroll_offset: UiPoint::new(0.0, state.virtual_table_scroll),
8356 overscan_rows: 1,
8357 },
8358 table_options,
8359 |ui, cell_parent, cell| {
8360 let source_row = visible_rows.get(cell.row).copied().unwrap_or(cell.row);
8361 let value = virtual_table_cell_value(source_row, cell.column);
8362 widgets::label(
8363 ui,
8364 cell_parent,
8365 format!(
8366 "lists_tables.virtualized_table.cell.{}.{}.label",
8367 cell.row, cell.column
8368 ),
8369 value,
8370 text(12.0, color(220, 228, 238)),
8371 LayoutStyle::new().with_width_percent(1.0),
8372 );
8373 },
8374 );
8375}
8376
8377#[allow(clippy::field_reassign_with_default)]
8378fn property_inspector(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
8379 let body = section(ui, parent, "property_inspector", "Property inspector");
8380 widgets::label(
8381 ui,
8382 body,
8383 "property_inspector.target",
8384 "Inspecting: Styling preview",
8385 text(12.0, color(196, 210, 230)),
8386 LayoutStyle::new().with_width_percent(1.0),
8387 );
8388 let mut options = ext_widgets::PropertyInspectorOptions::default();
8389 options.selected_index = Some(0);
8390 options.label_width = 120.0;
8391 options.row_height = 30.0;
8392 ext_widgets::property_inspector_grid(
8393 ui,
8394 body,
8395 "property_inspector.grid",
8396 &[
8397 ext_widgets::PropertyGridRow::new("target", "Widget", "Button preview").read_only(),
8398 ext_widgets::PropertyGridRow::new(
8399 "inner",
8400 "Inner margin",
8401 format!("{:.0}px", state.styling.inner_margin),
8402 )
8403 .with_kind(ext_widgets::PropertyValueKind::Number),
8404 ext_widgets::PropertyGridRow::new(
8405 "outer",
8406 "Outer margin",
8407 format!("{:.0}px", state.styling.outer_margin),
8408 )
8409 .with_kind(ext_widgets::PropertyValueKind::Number),
8410 ext_widgets::PropertyGridRow::new(
8411 "radius",
8412 "Corner radius",
8413 format!("{:.0}px", state.styling.corner_radius),
8414 )
8415 .with_kind(ext_widgets::PropertyValueKind::Number),
8416 ext_widgets::PropertyGridRow::new(
8417 "stroke",
8418 "Stroke",
8419 format!("{:.1}px", state.styling.stroke_width),
8420 )
8421 .with_kind(ext_widgets::PropertyValueKind::Number)
8422 .changed(),
8423 ext_widgets::PropertyGridRow::new("state", "Source", "Styling widget").read_only(),
8424 ],
8425 options,
8426 );
8427}
8428
8429fn diagnostics_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
8430 let body = section(ui, parent, "diagnostics", "Diagnostics");
8431 let debug_snapshot = &state.diagnostics_snapshot;
8432
8433 diagnostics_selected_node_panel(ui, body, debug_snapshot);
8434 diagnostics_animation_panel(ui, body, state, debug_snapshot);
8435
8436 widgets::label(
8437 ui,
8438 body,
8439 "diagnostics.a11y.title",
8440 "Accessibility",
8441 text(14.0, color(222, 230, 240)),
8442 LayoutStyle::new().with_width_percent(1.0),
8443 );
8444 let mut overlay_preview_style = UiNodeStyle::from(
8445 LayoutStyle::new()
8446 .with_width(320.0)
8447 .with_height(140.0)
8448 .with_flex_shrink(0.0),
8449 );
8450 overlay_preview_style.set_clip(ClipBehavior::Clip);
8451 let overlay_preview = ui.add_child(
8452 body,
8453 UiNode::container("diagnostics.a11y.preview", overlay_preview_style).with_visual(
8454 UiVisual::panel(
8455 color(12, 17, 24),
8456 Some(StrokeStyle::new(color(47, 62, 82), 1.0)),
8457 4.0,
8458 ),
8459 ),
8460 );
8461 let mut overlay_options = ext_widgets::AccessibilityDebugOverlayOptions {
8462 action_prefix: Some("diagnostics.a11y.visual".to_owned()),
8463 ..Default::default()
8464 };
8465 overlay_options.show_labels = false;
8466 ext_widgets::accessibility_debug_overlay(
8467 ui,
8468 overlay_preview,
8469 "diagnostics.a11y.visual",
8470 &debug_snapshot,
8471 overlay_options,
8472 );
8473 diagnostics_accessibility_details(ui, body, debug_snapshot);
8474
8475 let diagnostic_columns = ui.add_child(
8476 body,
8477 UiNode::container(
8478 "diagnostics.columns",
8479 LayoutStyle::column()
8480 .with_width_percent(1.0)
8481 .with_flex_shrink(0.0)
8482 .gap(10.0),
8483 ),
8484 );
8485 let command_column = ui.add_child(
8486 diagnostic_columns,
8487 UiNode::container(
8488 "diagnostics.commands.column",
8489 LayoutStyle::column()
8490 .with_width_percent(1.0)
8491 .with_flex_shrink(0.0)
8492 .gap(8.0),
8493 ),
8494 );
8495 let theme_column = ui.add_child(
8496 diagnostic_columns,
8497 UiNode::container(
8498 "diagnostics.theme.column",
8499 LayoutStyle::column()
8500 .with_width_percent(1.0)
8501 .with_flex_shrink(0.0)
8502 .gap(8.0),
8503 ),
8504 );
8505
8506 let registry = diagnostics_command_registry();
8507 diagnostics_commands_panel(ui, command_column, ®istry);
8508
8509 let theme_snapshot = DebugThemeSnapshot::from_theme(&Theme::dark());
8510 diagnostics_theme_panel(ui, theme_column, &theme_snapshot);
8511}
8512
8513fn diagnostics_selected_node_panel(
8514 ui: &mut UiDocument,
8515 parent: UiNodeId,
8516 snapshot: &DebugInspectorSnapshot,
8517) {
8518 let panel = diagnostics_panel(ui, parent, "diagnostics.inspector", "Selected node");
8519 let rows = snapshot
8520 .node("diagnostics.sample.preview")
8521 .map(|node| {
8522 vec![
8523 ext_widgets::PropertyGridRow::new("name", "Node", "Preview action").read_only(),
8524 ext_widgets::PropertyGridRow::new("role", "Role", "Button").read_only(),
8525 ext_widgets::PropertyGridRow::new(
8526 "bounds",
8527 "Bounds",
8528 format!(
8529 "{:.0}, {:.0}; {:.0} x {:.0}",
8530 node.rect.x, node.rect.y, node.rect.width, node.rect.height
8531 ),
8532 )
8533 .with_kind(ext_widgets::PropertyValueKind::Number)
8534 .read_only(),
8535 ext_widgets::PropertyGridRow::new(
8536 "clip",
8537 "Clip",
8538 format!("{:.0} x {:.0}", node.clip_rect.width, node.clip_rect.height),
8539 )
8540 .with_kind(ext_widgets::PropertyValueKind::Number)
8541 .read_only(),
8542 ext_widgets::PropertyGridRow::new(
8543 "input",
8544 "Input",
8545 if node.input.pointer {
8546 "Receives pointer input"
8547 } else {
8548 "Passive"
8549 },
8550 )
8551 .read_only(),
8552 ]
8553 })
8554 .unwrap_or_else(|| {
8555 vec![
8556 ext_widgets::PropertyGridRow::new("missing", "Selected node", "No node selected")
8557 .read_only(),
8558 ]
8559 });
8560 ext_widgets::property_inspector_grid(
8561 ui,
8562 panel,
8563 "diagnostics.inspector.rows",
8564 &rows,
8565 diagnostics_grid_options("Selected node details"),
8566 );
8567}
8568
8569fn diagnostics_animation_panel(
8570 ui: &mut UiDocument,
8571 parent: UiNodeId,
8572 state: &ShowcaseState,
8573 snapshot: &DebugInspectorSnapshot,
8574) {
8575 let graph_panel =
8576 diagnostics_panel(ui, parent, "diagnostics.animation.graph", "Animation state");
8577 if let Some(animation) = snapshot.animation("diagnostics.sample.preview") {
8578 let state_row = row(ui, graph_panel, "diagnostics.animation.graph.states", 8.0);
8579 for state_name in ["idle", "hot"] {
8580 diagnostic_chip(
8581 ui,
8582 state_row,
8583 format!("diagnostics.animation.graph.state.{state_name}"),
8584 state_name,
8585 animation.current_state == state_name,
8586 );
8587 }
8588
8589 let graph = animation.state_graph();
8590 for (index, edge) in graph.edges.iter().take(2).enumerate() {
8591 let value = if edge.kind == DebugAnimationGraphEdgeKind::Blend {
8592 "Input blend"
8593 } else {
8594 "State change"
8595 };
8596 let detail = if edge.label.is_empty() {
8597 if edge.active { "Active" } else { "Inactive" }.to_owned()
8598 } else if edge.active {
8599 format!("{}; active", edge.label)
8600 } else {
8601 edge.label.clone()
8602 };
8603 diagnostic_value_row(
8604 ui,
8605 graph_panel,
8606 format!("diagnostics.animation.graph.edge.{index}"),
8607 value,
8608 format!("{} -> {}", edge.from, edge.to),
8609 );
8610 diagnostic_muted_label(
8611 ui,
8612 graph_panel,
8613 format!("diagnostics.animation.graph.edge.{index}.detail"),
8614 detail,
8615 );
8616 }
8617 } else {
8618 diagnostic_muted_label(
8619 ui,
8620 graph_panel,
8621 "diagnostics.animation.graph.empty",
8622 "No animation state machine",
8623 );
8624 }
8625
8626 let controls_panel = diagnostics_panel(
8627 ui,
8628 parent,
8629 "diagnostics.animation.controls",
8630 "Animation controls",
8631 );
8632 let transport = row(
8633 ui,
8634 controls_panel,
8635 "diagnostics.animation.controls.transport",
8636 8.0,
8637 );
8638 diagnostic_button(
8639 ui,
8640 transport,
8641 "diagnostics.animation.controls.transport.pause_toggle",
8642 if state.diagnostics_animation_paused {
8643 "Resume"
8644 } else {
8645 "Pause"
8646 },
8647 state.diagnostics_animation_paused,
8648 );
8649 diagnostic_button(
8650 ui,
8651 transport,
8652 "diagnostics.animation.controls.transport.step",
8653 "Step",
8654 false,
8655 );
8656 diagnostic_slider_row(
8657 ui,
8658 controls_panel,
8659 "diagnostics.animation.controls.transport.scrub",
8660 "Scrub progress",
8661 state.diagnostics_animation_scrub,
8662 "diagnostics.animation.controls.transport.scrub",
8663 );
8664 diagnostic_button(
8665 ui,
8666 controls_panel,
8667 "diagnostics.animation.controls.input.active.toggle",
8668 if state.diagnostics_animation_active {
8669 "Active input: true"
8670 } else {
8671 "Active input: false"
8672 },
8673 state.diagnostics_animation_active,
8674 );
8675 diagnostic_slider_row(
8676 ui,
8677 controls_panel,
8678 "diagnostics.animation.controls.input.hover.set",
8679 "Hover blend",
8680 state.diagnostics_animation_hover,
8681 "diagnostics.animation.controls.input.hover.set",
8682 );
8683 diagnostic_button(
8684 ui,
8685 controls_panel,
8686 "diagnostics.animation.controls.input.pulse.fire",
8687 "Fire pulse",
8688 false,
8689 );
8690 widgets::label(
8691 ui,
8692 controls_panel,
8693 "diagnostics.animation.controls.status",
8694 format!(
8695 "Scrub {:.0}% Hover {:.0}% Pulses {}",
8696 state.diagnostics_animation_scrub * 100.0,
8697 state.diagnostics_animation_hover * 100.0,
8698 state.diagnostics_animation_pulse_count
8699 ),
8700 text(12.0, color(166, 180, 198)),
8701 LayoutStyle::new().with_width_percent(1.0),
8702 );
8703}
8704
8705fn diagnostics_accessibility_details(
8706 ui: &mut UiDocument,
8707 parent: UiNodeId,
8708 snapshot: &DebugInspectorSnapshot,
8709) {
8710 let rows = snapshot
8711 .accessibility_overlay
8712 .iter()
8713 .find(|node| node.name == "diagnostics.sample.preview")
8714 .map(|node| {
8715 let accessibility = node.accessibility.as_ref();
8716 vec![
8717 ext_widgets::PropertyGridRow::new("role", "Role", "Button").read_only(),
8718 ext_widgets::PropertyGridRow::new(
8719 "label",
8720 "Label",
8721 accessibility
8722 .and_then(|meta| meta.label.clone())
8723 .unwrap_or_else(|| "Preview action".to_owned()),
8724 )
8725 .read_only(),
8726 ext_widgets::PropertyGridRow::new(
8727 "focus",
8728 "Focus order",
8729 node.focus_index
8730 .map(|index| format!("#{}", index + 1))
8731 .unwrap_or_else(|| "Not focusable".to_owned()),
8732 )
8733 .read_only(),
8734 ext_widgets::PropertyGridRow::new(
8735 "warnings",
8736 "Warnings",
8737 if node.warnings.is_empty() {
8738 "None"
8739 } else {
8740 "Needs review"
8741 },
8742 )
8743 .read_only(),
8744 ]
8745 })
8746 .unwrap_or_else(|| {
8747 vec![
8748 ext_widgets::PropertyGridRow::new("missing", "Accessibility", "No metadata")
8749 .read_only(),
8750 ]
8751 });
8752 ext_widgets::property_inspector_grid(
8753 ui,
8754 parent,
8755 "diagnostics.a11y",
8756 &rows,
8757 diagnostics_grid_options("Accessibility metadata"),
8758 );
8759}
8760
8761fn diagnostics_commands_panel(ui: &mut UiDocument, parent: UiNodeId, registry: &CommandRegistry) {
8762 let panel = diagnostics_panel(ui, parent, "diagnostics.commands", "Commands");
8763 let formatter = ShortcutFormatter::default();
8764 for command_id in [
8765 "diagnostics.palette",
8766 "diagnostics.inspect",
8767 "diagnostics.record",
8768 "diagnostics.export_theme",
8769 ] {
8770 if let Some(command) = registry.command(command_id) {
8771 let shortcut = registry
8772 .command_bindings(command.meta.id.clone())
8773 .first()
8774 .map(|binding| formatter.format(binding.shortcut))
8775 .unwrap_or_else(|| "Unbound".to_owned());
8776 let status = if command.enabled {
8777 command
8778 .meta
8779 .category
8780 .clone()
8781 .unwrap_or_else(|| "General".to_owned())
8782 } else {
8783 command
8784 .disabled_reason
8785 .clone()
8786 .unwrap_or_else(|| "Disabled".to_owned())
8787 };
8788 diagnostic_command_row(
8789 ui,
8790 panel,
8791 format!(
8792 "diagnostics.commands.row.{}",
8793 command.meta.id.as_str().replace('.', "_")
8794 ),
8795 &command.meta.label,
8796 &shortcut,
8797 &status,
8798 );
8799 }
8800 }
8801 diagnostic_value_row(
8802 ui,
8803 panel,
8804 "diagnostics.commands.conflicts",
8805 "Shortcut conflicts",
8806 if registry.conflicts().is_empty() {
8807 "None"
8808 } else {
8809 "Needs review"
8810 },
8811 );
8812}
8813
8814fn diagnostics_theme_panel(ui: &mut UiDocument, parent: UiNodeId, snapshot: &DebugThemeSnapshot) {
8815 let panel = diagnostics_panel(ui, parent, "diagnostics.theme", "Theme tokens");
8816 diagnostic_value_row(
8817 ui,
8818 panel,
8819 "diagnostics.theme.name",
8820 "Theme",
8821 snapshot.name.as_str(),
8822 );
8823 for token_path in ["colors.accent", "colors.surface", "typography.body"] {
8824 if let Some(token) = snapshot.token(token_path) {
8825 diagnostic_value_row(
8826 ui,
8827 panel,
8828 format!("diagnostics.theme.token.{}", token_path.replace('.', "_")),
8829 token_path,
8830 token.value.as_str(),
8831 );
8832 }
8833 }
8834 if let Some(component) = snapshot.component_states.first() {
8835 diagnostic_value_row(
8836 ui,
8837 panel,
8838 "diagnostics.theme.component.button",
8839 "Button normal",
8840 format!(
8841 "{:.0} x {:.0}, padding {:.0}",
8842 component.min_width, component.min_height, component.padding_x
8843 ),
8844 );
8845 }
8846}
8847
8848fn diagnostics_panel(
8849 ui: &mut UiDocument,
8850 parent: UiNodeId,
8851 name: impl Into<String>,
8852 title: impl Into<String>,
8853) -> UiNodeId {
8854 let name = name.into();
8855 let title = title.into();
8856 let panel = ui.add_child(
8857 parent,
8858 UiNode::container(
8859 name.clone(),
8860 LayoutStyle::column()
8861 .with_width_percent(1.0)
8862 .with_padding(10.0)
8863 .with_gap(8.0)
8864 .with_flex_shrink(0.0),
8865 )
8866 .with_visual(UiVisual::panel(
8867 color(15, 20, 28),
8868 Some(StrokeStyle::new(color(52, 65, 84), 1.0)),
8869 4.0,
8870 ))
8871 .with_accessibility(AccessibilityMeta::new(AccessibilityRole::Group).label(title.clone())),
8872 );
8873 widgets::label(
8874 ui,
8875 panel,
8876 format!("{name}.title"),
8877 title,
8878 text(13.0, color(222, 230, 240)),
8879 LayoutStyle::new().with_width_percent(1.0),
8880 );
8881 panel
8882}
8883
8884fn diagnostics_grid_options(label: impl Into<String>) -> ext_widgets::PropertyInspectorOptions {
8885 ext_widgets::PropertyInspectorOptions {
8886 label_width: 112.0,
8887 row_height: 28.0,
8888 accessibility_label: Some(label.into()),
8889 ..Default::default()
8890 }
8891}
8892
8893fn diagnostic_value_row(
8894 ui: &mut UiDocument,
8895 parent: UiNodeId,
8896 name: impl Into<String>,
8897 label: impl Into<String>,
8898 value: impl Into<String>,
8899) -> UiNodeId {
8900 let name = name.into();
8901 let row = row(ui, parent, name.clone(), 8.0);
8902 widgets::label(
8903 ui,
8904 row,
8905 format!("{name}.label"),
8906 label.into(),
8907 text(12.0, color(166, 180, 198)),
8908 LayoutStyle::new().with_width(136.0).with_flex_shrink(0.0),
8909 );
8910 widgets::label(
8911 ui,
8912 row,
8913 format!("{name}.value"),
8914 value.into(),
8915 text(12.0, color(226, 234, 244)),
8916 LayoutStyle::new().with_width_percent(1.0),
8917 );
8918 row
8919}
8920
8921fn diagnostic_muted_label(
8922 ui: &mut UiDocument,
8923 parent: UiNodeId,
8924 name: impl Into<String>,
8925 label: impl Into<String>,
8926) -> UiNodeId {
8927 let mut style = text(12.0, color(166, 180, 198));
8928 style.wrap = TextWrap::WordOrGlyph;
8929 widgets::label(
8930 ui,
8931 parent,
8932 name,
8933 label.into(),
8934 style,
8935 LayoutStyle::new().with_width_percent(1.0),
8936 )
8937}
8938
8939fn diagnostic_command_row(
8940 ui: &mut UiDocument,
8941 parent: UiNodeId,
8942 name: impl Into<String>,
8943 label: &str,
8944 shortcut: &str,
8945 status: &str,
8946) -> UiNodeId {
8947 let name = name.into();
8948 let row = row(ui, parent, name.clone(), 8.0);
8949 widgets::label(
8950 ui,
8951 row,
8952 format!("{name}.label"),
8953 label,
8954 text(12.0, color(226, 234, 244)),
8955 LayoutStyle::new()
8956 .with_width_percent(1.0)
8957 .with_flex_grow(1.0),
8958 );
8959 widgets::label(
8960 ui,
8961 row,
8962 format!("{name}.shortcut"),
8963 shortcut,
8964 text(12.0, color(166, 180, 198)),
8965 LayoutStyle::new().with_width(78.0).with_flex_shrink(0.0),
8966 );
8967 widgets::label(
8968 ui,
8969 row,
8970 format!("{name}.status"),
8971 status,
8972 text(12.0, color(166, 180, 198)),
8973 LayoutStyle::new().with_width(140.0).with_flex_shrink(0.0),
8974 );
8975 row
8976}
8977
8978fn diagnostic_slider_row(
8979 ui: &mut UiDocument,
8980 parent: UiNodeId,
8981 name: impl Into<String>,
8982 label: impl Into<String>,
8983 value: f32,
8984 action: impl Into<String>,
8985) -> UiNodeId {
8986 let name = name.into();
8987 let label = label.into();
8988 let row = row(ui, parent, format!("{name}.row"), 8.0);
8989 widgets::label(
8990 ui,
8991 row,
8992 format!("{name}.label"),
8993 label.clone(),
8994 text(12.0, color(166, 180, 198)),
8995 LayoutStyle::new().with_width(136.0).with_flex_shrink(0.0),
8996 );
8997 let slider_name = if name.ends_with(".set") {
8998 format!("{name}.slider")
8999 } else {
9000 name.clone()
9001 };
9002 let mut options = widgets::SliderOptions::default()
9003 .with_layout(LayoutStyle::new().with_width(160.0).with_height(24.0))
9004 .with_value_edit_action(action.into());
9005 options.accessibility_label = Some(label);
9006 widgets::slider(ui, row, slider_name, value, 0.0..1.0, options);
9007 widgets::label(
9008 ui,
9009 row,
9010 format!("{name}.percent"),
9011 format!("{:.0}%", value * 100.0),
9012 text(12.0, color(226, 234, 244)),
9013 LayoutStyle::new().with_width(46.0).with_flex_shrink(0.0),
9014 );
9015 row
9016}
9017
9018fn diagnostic_button(
9019 ui: &mut UiDocument,
9020 parent: UiNodeId,
9021 name: impl Into<String>,
9022 label: impl Into<String>,
9023 active: bool,
9024) -> UiNodeId {
9025 let name = name.into();
9026 let mut options = widgets::ButtonOptions::default()
9027 .with_layout(LayoutStyle::new().with_height(32.0))
9028 .with_action(name.clone())
9029 .pressed(active);
9030 if active {
9031 options.visual = UiVisual::panel(
9032 color(47, 94, 150),
9033 Some(StrokeStyle::new(color(103, 164, 224), 1.0)),
9034 4.0,
9035 );
9036 }
9037 widgets::button(ui, parent, name, label, options)
9038}
9039
9040fn diagnostic_chip(
9041 ui: &mut UiDocument,
9042 parent: UiNodeId,
9043 name: impl Into<String>,
9044 label: impl Into<String>,
9045 active: bool,
9046) -> UiNodeId {
9047 let name = name.into();
9048 let chip = ui.add_child(
9049 parent,
9050 UiNode::container(
9051 name.clone(),
9052 LayoutStyle::new()
9053 .with_width(82.0)
9054 .with_height(28.0)
9055 .with_padding(4.0)
9056 .with_flex_shrink(0.0),
9057 )
9058 .with_visual(if active {
9059 UiVisual::panel(
9060 color(47, 94, 150),
9061 Some(StrokeStyle::new(color(103, 164, 224), 1.0)),
9062 4.0,
9063 )
9064 } else {
9065 UiVisual::panel(
9066 color(31, 39, 50),
9067 Some(StrokeStyle::new(color(62, 76, 96), 1.0)),
9068 4.0,
9069 )
9070 }),
9071 );
9072 widgets::label(
9073 ui,
9074 chip,
9075 format!("{name}.label"),
9076 label.into(),
9077 text(12.0, color(226, 234, 244)),
9078 LayoutStyle::new().with_width_percent(1.0),
9079 );
9080 chip
9081}
9082
9083fn diagnostics_sample_snapshot(state: &ShowcaseState) -> DebugInspectorSnapshot {
9084 diagnostics_sample_snapshot_for(
9085 state.diagnostics_animation_hover,
9086 state.diagnostics_animation_active,
9087 )
9088}
9089
9090fn diagnostics_sample_snapshot_for(hover: f32, active: bool) -> DebugInspectorSnapshot {
9091 let mut sample = UiDocument::new(root_style(320.0, 180.0));
9092 let card = sample.add_child(
9093 sample.root(),
9094 UiNode::container(
9095 "diagnostics.sample.card",
9096 LayoutStyle::column()
9097 .with_width_percent(1.0)
9098 .with_height(120.0)
9099 .padding(12.0)
9100 .gap(8.0),
9101 )
9102 .with_visual(UiVisual::panel(
9103 color(16, 22, 30),
9104 Some(StrokeStyle::new(color(62, 77, 98), 1.0)),
9105 6.0,
9106 ))
9107 .with_accessibility(
9108 AccessibilityMeta::new(AccessibilityRole::Group).label("Diagnostics sample"),
9109 ),
9110 );
9111 sample.add_child(
9112 card,
9113 UiNode::container(
9114 "diagnostics.sample.preview",
9115 LayoutStyle::new().with_width(160.0).with_height(38.0),
9116 )
9117 .with_input(InputBehavior::BUTTON)
9118 .with_visual(UiVisual::panel(
9119 color(52, 112, 180),
9120 Some(StrokeStyle::new(color(116, 183, 255), 1.0)),
9121 5.0,
9122 ))
9123 .with_accessibility(
9124 AccessibilityMeta::new(AccessibilityRole::Button)
9125 .label("Preview action")
9126 .focusable(),
9127 )
9128 .with_animation(
9129 AnimationMachine::new(
9130 vec![
9131 AnimationState::new(
9132 "idle",
9133 AnimatedValues::new(1.0, UiPoint::new(0.0, 0.0), 1.0),
9134 ),
9135 AnimationState::new(
9136 "hot",
9137 AnimatedValues::new(0.92, UiPoint::new(18.0, 0.0), 1.08),
9138 ),
9139 ],
9140 vec![AnimationTransition::when(
9141 "idle",
9142 "hot",
9143 AnimationCondition::bool("active", true),
9144 0.18,
9145 )],
9146 "idle",
9147 )
9148 .expect("sample animation")
9149 .with_number_input("hover", hover)
9150 .with_blend_binding(AnimationBlendBinding::new("hover", "idle", "hot"))
9151 .with_bool_input("active", active)
9152 .with_trigger_input("pulse"),
9153 ),
9154 );
9155 widgets::label(
9156 &mut sample,
9157 card,
9158 "diagnostics.sample.label",
9159 "Sample node",
9160 text(12.0, color(198, 210, 226)),
9161 LayoutStyle::new().with_width_percent(1.0),
9162 );
9163 sample
9164 .compute_layout(UiSize::new(320.0, 180.0), &mut ApproxTextMeasurer)
9165 .expect("sample layout");
9166 DebugInspectorSnapshot::from_document(&sample, &mut ApproxTextMeasurer)
9167}
9168
9169fn diagnostics_command_registry() -> CommandRegistry {
9170 let mut registry = CommandRegistry::new();
9171 registry
9172 .register(
9173 CommandMeta::new("diagnostics.palette", "Open command palette")
9174 .description("Show command search")
9175 .category("Debug"),
9176 )
9177 .expect("command");
9178 registry
9179 .register(
9180 CommandMeta::new("diagnostics.inspect", "Inspect selected node")
9181 .description("Focus the layout inspector")
9182 .category("Debug"),
9183 )
9184 .expect("command");
9185 registry
9186 .register(
9187 CommandMeta::new("diagnostics.record", "Start interaction recording")
9188 .description("Capture replay steps")
9189 .category("Testing"),
9190 )
9191 .expect("command");
9192 registry
9193 .register(CommandMeta::new(
9194 "diagnostics.export_theme",
9195 "Export theme patch",
9196 ))
9197 .expect("command");
9198 registry
9199 .bind_shortcut(
9200 CommandScope::Global,
9201 Shortcut::ctrl('k'),
9202 "diagnostics.palette",
9203 )
9204 .expect("shortcut");
9205 registry
9206 .bind_shortcut(
9207 CommandScope::Panel,
9208 Shortcut::ctrl('i'),
9209 "diagnostics.inspect",
9210 )
9211 .expect("shortcut");
9212 registry
9213 .bind_shortcut(
9214 CommandScope::Panel,
9215 Shortcut::ctrl('r'),
9216 "diagnostics.record",
9217 )
9218 .expect("shortcut");
9219 registry
9220 .disable("diagnostics.export_theme", "No changes to export")
9221 .expect("disable");
9222 registry
9223}
9224
9225fn tree_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9226 let body = section(ui, parent, "trees", "Tree view");
9227 widgets::label(
9228 ui,
9229 body,
9230 "trees.tree_view.title",
9231 "Editable tree",
9232 text(12.0, color(166, 176, 190)),
9233 LayoutStyle::new().with_width_percent(1.0),
9234 );
9235 ext_widgets::tree_view(
9236 ui,
9237 body,
9238 "trees.tree_view",
9239 &editable_tree_items(&state.editable_tree),
9240 &state.tree,
9241 ext_widgets::TreeViewOptions::default().with_row_action_prefix("trees.tree"),
9242 );
9243 widgets::label(
9244 ui,
9245 body,
9246 "trees.editable.status",
9247 &state.editable_tree_status,
9248 text(12.0, color(154, 166, 184)),
9249 LayoutStyle::new().with_width_percent(1.0),
9250 );
9251 widgets::label(
9252 ui,
9253 body,
9254 "trees.virtual.title",
9255 "Virtualized tree",
9256 text(12.0, color(166, 176, 190)),
9257 LayoutStyle::new().with_width_percent(1.0),
9258 );
9259 let virtual_nodes = ext_widgets::virtualized_tree_view(
9260 ui,
9261 body,
9262 "trees.virtual",
9263 &virtual_tree_items(),
9264 &state.tree_virtual,
9265 ext_widgets::VirtualTreeViewSpec::new(24.0, 112.0)
9266 .scroll_offset(state.tree_virtual_scroll)
9267 .overscan_rows(1),
9268 ext_widgets::TreeViewOptions::default().with_row_action_prefix("trees.virtual"),
9269 );
9270 ui.node_mut(virtual_nodes.body)
9271 .set_action("trees.virtual.scroll");
9272 widgets::label(
9273 ui,
9274 body,
9275 "trees.table.title",
9276 "Tree table",
9277 text(12.0, color(166, 176, 190)),
9278 LayoutStyle::new().with_width_percent(1.0),
9279 );
9280 tree_table_widgets(ui, body, state);
9281}
9282
9283fn tree_table_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9284 let rows = state.tree_table.visible_items(&tree_table_items());
9285 let columns = [
9286 ext_widgets::DataTableColumn::new("name", "Name", 220.0),
9287 ext_widgets::DataTableColumn::new("kind", "Kind", 84.0),
9288 ext_widgets::DataTableColumn::new("status", "Status", 92.0),
9289 ];
9290 let mut options = ext_widgets::DataTableOptions::default()
9291 .with_row_action_prefix("trees.table")
9292 .with_cell_action_prefix("trees.table")
9293 .with_scroll_action("trees.table.scroll");
9294 options.selection = state
9295 .tree_table
9296 .selected_index()
9297 .map(ext_widgets::DataTableSelection::single_row)
9298 .unwrap_or_default();
9299 options.layout = LayoutStyle::column()
9300 .with_width_percent(1.0)
9301 .with_height(132.0)
9302 .with_flex_shrink(0.0);
9303 ext_widgets::virtualized_data_table(
9304 ui,
9305 parent,
9306 "trees.table",
9307 &columns,
9308 ext_widgets::VirtualDataTableSpec {
9309 row_count: rows.len(),
9310 row_height: 24.0,
9311 viewport_width: 396.0,
9312 viewport_height: 96.0,
9313 scroll_offset: UiPoint::new(0.0, state.tree_table_scroll),
9314 overscan_rows: 1,
9315 },
9316 options,
9317 |ui, cell_parent, cell| {
9318 let Some(item) = rows.get(cell.row) else {
9319 return;
9320 };
9321 if cell.column == 0 {
9322 tree_table_name_cell(ui, cell_parent, cell.row, item);
9323 } else {
9324 widgets::label(
9325 ui,
9326 cell_parent,
9327 format!("trees.table.cell.{}.{}.label", cell.row, cell.column),
9328 tree_table_cell_value(item, cell.column),
9329 text(12.0, color(220, 228, 238)),
9330 LayoutStyle::new().with_width_percent(1.0),
9331 );
9332 }
9333 },
9334 );
9335}
9336
9337fn tree_table_name_cell(
9338 ui: &mut UiDocument,
9339 parent: UiNodeId,
9340 row: usize,
9341 item: &ext_widgets::TreeVisibleItem,
9342) {
9343 if item.depth > 0 {
9344 ui.add_child(
9345 parent,
9346 UiNode::container(
9347 format!("trees.table.row.{}.indent", item.id),
9348 LayoutStyle::new()
9349 .with_width(item.depth as f32 * 16.0)
9350 .with_height_percent(1.0)
9351 .with_flex_shrink(0.0),
9352 ),
9353 );
9354 }
9355 widgets::label(
9356 ui,
9357 parent,
9358 format!("trees.table.row.{}.disclosure", item.id),
9359 if item.has_children() {
9360 if item.expanded {
9361 "v"
9362 } else {
9363 ">"
9364 }
9365 } else {
9366 ""
9367 },
9368 text(12.0, color(166, 176, 190)),
9369 LayoutStyle::new()
9370 .with_width(18.0)
9371 .with_height_percent(1.0)
9372 .with_flex_shrink(0.0),
9373 );
9374 widgets::label(
9375 ui,
9376 parent,
9377 format!("trees.table.cell.{row}.0.label"),
9378 item.label.clone(),
9379 if item.disabled {
9380 text(12.0, color(154, 166, 184))
9381 } else {
9382 text(12.0, color(220, 228, 238))
9383 },
9384 LayoutStyle::new().with_width_percent(1.0),
9385 );
9386}
9387
9388fn tree_table_cell_value(item: &ext_widgets::TreeVisibleItem, column: usize) -> String {
9389 match column {
9390 0 => item.label.clone(),
9391 1 => {
9392 if item.has_children() {
9393 "Folder".to_owned()
9394 } else {
9395 "File".to_owned()
9396 }
9397 }
9398 _ => {
9399 if item.disabled {
9400 "Locked".to_owned()
9401 } else if item.has_children() && item.expanded {
9402 "Expanded".to_owned()
9403 } else if item.has_children() {
9404 "Collapsed".to_owned()
9405 } else if item.expanded {
9406 "Expanded".to_owned()
9407 } else {
9408 "Ready".to_owned()
9409 }
9410 }
9411 }
9412}
9413
9414fn tab_split_dock_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9415 let body = section_with_min_viewport(
9416 ui,
9417 parent,
9418 "layout_widgets",
9419 "Layout widgets",
9420 UiSize::new(640.0, 360.0),
9421 );
9422 let shell = ui.add_child(
9423 body,
9424 UiNode::container(
9425 "layout_widgets.dock_shell",
9426 LayoutStyle::column()
9427 .with_width_percent(1.0)
9428 .with_height(360.0)
9429 .with_flex_shrink(0.0),
9430 )
9431 .with_visual(UiVisual::panel(
9432 color(13, 17, 23),
9433 Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
9434 0.0,
9435 )),
9436 );
9437
9438 let mut panels = base_layout_dock_panels();
9439 state.layout_dock.apply_order_to_panels(&mut panels);
9440 state.layout_dock.apply_visibility_to_panels(&mut panels);
9441
9442 let mut drawer_options = ext_widgets::DockDrawerRailOptions::default();
9443 drawer_options.layout = LayoutStyle::row()
9444 .with_width_percent(1.0)
9445 .with_height(34.0)
9446 .with_padding(4.0)
9447 .with_gap(4.0);
9448 ext_widgets::dock_drawer_rail(
9449 ui,
9450 shell,
9451 "layout_widgets.dock.drawers",
9452 &[
9453 ext_widgets::DockDrawerDescriptor::new(
9454 "panel_a",
9455 "Panel A",
9456 "panel_a",
9457 ext_widgets::DockSide::Left,
9458 )
9459 .open(!state.layout_dock.is_hidden("panel_a"))
9460 .with_action("layout_widgets.drawer.panel_a"),
9461 ext_widgets::DockDrawerDescriptor::new(
9462 "panel_b",
9463 "Panel B",
9464 "panel_b",
9465 ext_widgets::DockSide::Right,
9466 )
9467 .open(!state.layout_dock.is_hidden("panel_b"))
9468 .with_action("layout_widgets.drawer.panel_b"),
9469 ],
9470 drawer_options,
9471 );
9472
9473 let mut options = ext_widgets::DockWorkspaceOptions::default();
9474 options.layout = LayoutStyle::column()
9475 .with_width_percent(1.0)
9476 .with_height(0.0)
9477 .with_flex_grow(1.0);
9478 options.show_titles = true;
9479 options.handle_thickness = 2.0;
9480 options.panel_visual = UiVisual::panel(
9481 color(18, 22, 29),
9482 Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
9483 0.0,
9484 );
9485 options.center_visual = UiVisual::panel(
9486 color(15, 19, 25),
9487 Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
9488 0.0,
9489 );
9490 options.resize_handle_visual = UiVisual::panel(color(65, 78, 96), None, 0.0);
9491
9492 ext_widgets::dock_workspace(
9493 ui,
9494 shell,
9495 "layout_widgets.dock",
9496 &panels,
9497 options,
9498 |ui, parent, panel| match panel.id.as_str() {
9499 "panel_a" => layout_panel_contents(
9500 ui,
9501 parent,
9502 "layout.panel_a",
9503 state.layout_panel_a_scroll,
9504 &["Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6"],
9505 ),
9506 "workspace" => layout_workspace_contents(
9507 ui,
9508 parent,
9509 "layout.workspace",
9510 state.layout_workspace_scroll,
9511 ),
9512 "panel_b" => layout_panel_contents(
9513 ui,
9514 parent,
9515 "layout.panel_b",
9516 state.layout_panel_b_scroll,
9517 &[
9518 "Value A", "Value B", "Value C", "Value D", "Value E", "Value F",
9519 ],
9520 ),
9521 _ => {}
9522 },
9523 );
9524
9525 if let Some(floating) = state.layout_dock.floating_panel("panel_a") {
9526 let floating_panel = ui.add_child(
9527 shell,
9528 UiNode::container(
9529 "layout_widgets.floating.panel_a",
9530 operad::layout::absolute(
9531 floating.rect.x,
9532 floating.rect.y,
9533 floating.rect.width,
9534 floating.rect.height,
9535 ),
9536 )
9537 .with_visual(UiVisual::panel(
9538 color(18, 22, 29),
9539 Some(StrokeStyle::new(color(86, 102, 124), 1.0)),
9540 4.0,
9541 )),
9542 );
9543 layout_panel_contents(
9544 ui,
9545 floating_panel,
9546 "layout.panel_a_floating",
9547 state.layout_panel_a_scroll,
9548 &["Item 1", "Item 2", "Item 3", "Item 4"],
9549 );
9550 }
9551}
9552
9553fn base_layout_dock_panels() -> Vec<ext_widgets::DockPanelDescriptor> {
9554 vec![
9555 ext_widgets::DockPanelDescriptor::new(
9556 "panel_a",
9557 "Panel A",
9558 ext_widgets::DockSide::Left,
9559 200.0,
9560 )
9561 .with_min_size(150.0)
9562 .resizable(true),
9563 ext_widgets::DockPanelDescriptor::center("workspace", "Workspace").with_min_size(220.0),
9564 ext_widgets::DockPanelDescriptor::new(
9565 "panel_b",
9566 "Panel B",
9567 ext_widgets::DockSide::Right,
9568 200.0,
9569 )
9570 .with_min_size(150.0)
9571 .resizable(true),
9572 ]
9573}
9574
9575fn container_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9576 let body = section_with_min_viewport(
9577 ui,
9578 parent,
9579 "containers",
9580 "Containers",
9581 UiSize::new(420.0, 0.0),
9582 );
9583
9584 let frame = widgets::frame(
9585 ui,
9586 body,
9587 "containers.frame",
9588 widgets::FrameOptions::default().with_layout(
9589 LayoutStyle::column()
9590 .with_width_percent(1.0)
9591 .with_height(64.0)
9592 .with_padding(8.0)
9593 .with_gap(6.0),
9594 ),
9595 );
9596 widgets::strong_label(
9597 ui,
9598 frame,
9599 "containers.frame.title",
9600 "Frame",
9601 LayoutStyle::new().with_width_percent(1.0),
9602 );
9603 widgets::weak_label(
9604 ui,
9605 frame,
9606 "containers.frame.body",
9607 "Framed surface with padding.",
9608 LayoutStyle::new().with_width_percent(1.0),
9609 );
9610
9611 let group = widgets::group(ui, body, "containers.group");
9612 widgets::label(
9613 ui,
9614 group,
9615 "containers.group.label",
9616 "Group helper",
9617 text(12.0, color(220, 228, 238)),
9618 LayoutStyle::new().with_width_percent(1.0),
9619 );
9620 let generic_panel = widgets::panel(
9621 ui,
9622 body,
9623 "containers.panel",
9624 widgets::PanelOptions::group().with_layout(
9625 LayoutStyle::column()
9626 .with_width_percent(1.0)
9627 .with_height(44.0)
9628 .with_padding(8.0),
9629 ),
9630 );
9631 widgets::label(
9632 ui,
9633 generic_panel,
9634 "containers.panel.label",
9635 "Generic panel",
9636 text(12.0, color(220, 228, 238)),
9637 LayoutStyle::new().with_width_percent(1.0),
9638 );
9639 let group_panel = widgets::group_panel(ui, body, "containers.group_panel");
9640 widgets::label(
9641 ui,
9642 group_panel,
9643 "containers.group_panel.label",
9644 "Group panel",
9645 text(12.0, color(220, 228, 238)),
9646 LayoutStyle::new().with_width_percent(1.0),
9647 );
9648
9649 widgets::separator(
9650 ui,
9651 body,
9652 "containers.separator",
9653 widgets::SeparatorOptions::default(),
9654 );
9655 widgets::spacer(
9656 ui,
9657 body,
9658 "containers.spacer",
9659 LayoutStyle::new()
9660 .with_width_percent(1.0)
9661 .with_height(8.0)
9662 .with_flex_shrink(0.0),
9663 );
9664
9665 let grid = widgets::grid::grid(
9666 ui,
9667 body,
9668 "containers.grid",
9669 widgets::grid::GridOptions::default().with_layout(
9670 LayoutStyle::column()
9671 .with_width_percent(1.0)
9672 .with_height(78.0)
9673 .with_gap(4.0),
9674 ),
9675 );
9676 for row_index in 0..2 {
9677 let row = widgets::grid::grid_row(
9678 ui,
9679 grid,
9680 format!("containers.grid.row.{row_index}"),
9681 widgets::grid::GridRowOptions::default(),
9682 );
9683 for column_index in 0..3 {
9684 widgets::grid::grid_text_cell(
9685 ui,
9686 row,
9687 format!("containers.grid.row.{row_index}.cell.{column_index}"),
9688 format!("R{} C{}", row_index + 1, column_index + 1),
9689 widgets::grid::GridCellOptions {
9690 text_style: text(12.0, color(214, 224, 238)),
9691 ..Default::default()
9692 },
9693 );
9694 }
9695 }
9696
9697 widgets::sides(
9698 ui,
9699 body,
9700 "containers.sides",
9701 widgets::SidesOptions::default()
9702 .with_layout(LayoutStyle::row().with_width_percent(1.0).with_height(48.0))
9703 .with_gap(8.0)
9704 .with_visual(UiVisual::panel(
9705 color(20, 25, 32),
9706 Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
9707 4.0,
9708 )),
9709 |ui, left| {
9710 widgets::label(
9711 ui,
9712 left,
9713 "containers.sides.left.label",
9714 "Left side",
9715 text(12.0, color(220, 228, 238)),
9716 LayoutStyle::new().with_width_percent(1.0),
9717 );
9718 },
9719 |ui, right| {
9720 widgets::label(
9721 ui,
9722 right,
9723 "containers.sides.right.label",
9724 "Right side",
9725 text(12.0, color(220, 228, 238)),
9726 LayoutStyle::new().with_width_percent(1.0),
9727 );
9728 },
9729 );
9730
9731 widgets::columns(
9732 ui,
9733 body,
9734 "containers.columns",
9735 3,
9736 widgets::ColumnsOptions::default()
9737 .with_layout(LayoutStyle::row().with_width_percent(1.0).with_height(48.0))
9738 .with_gap(8.0),
9739 |ui, column, index| {
9740 widgets::label(
9741 ui,
9742 column,
9743 format!("containers.columns.{index}.label"),
9744 format!("Column {}", index + 1),
9745 text(12.0, color(220, 228, 238)),
9746 LayoutStyle::new().with_width_percent(1.0),
9747 );
9748 },
9749 );
9750
9751 let indented = widgets::indented_section(
9752 ui,
9753 body,
9754 "containers.indented",
9755 widgets::IndentOptions::default().with_amount(24.0),
9756 );
9757 widgets::label(
9758 ui,
9759 indented,
9760 "containers.indented.label",
9761 "Indented section",
9762 text(12.0, color(196, 210, 230)),
9763 LayoutStyle::new().with_width_percent(1.0),
9764 );
9765
9766 widgets::resize_container(
9767 ui,
9768 body,
9769 "containers.resize_container",
9770 widgets::ResizeContainerOptions::default().with_layout(
9771 LayoutStyle::column()
9772 .with_width_percent(1.0)
9773 .with_height(92.0)
9774 .with_flex_shrink(0.0),
9775 ),
9776 |ui, content| {
9777 widgets::label(
9778 ui,
9779 content,
9780 "containers.resize_container.label",
9781 "Resize container",
9782 text(12.0, color(220, 228, 238)),
9783 LayoutStyle::new().with_width_percent(1.0),
9784 );
9785 },
9786 );
9787
9788 widgets::scene(
9789 ui,
9790 body,
9791 "containers.scene",
9792 vec![
9793 ScenePrimitive::Rect(
9794 PaintRect::solid(UiRect::new(8.0, 12.0, 108.0, 46.0), color(48, 112, 184))
9795 .stroke(AlignedStroke::inside(StrokeStyle::new(
9796 color(132, 174, 222),
9797 1.0,
9798 )))
9799 .corner_radii(CornerRadii::uniform(6.0)),
9800 ),
9801 ScenePrimitive::Circle {
9802 center: UiPoint::new(150.0, 35.0),
9803 radius: 22.0,
9804 fill: color(111, 203, 159),
9805 stroke: Some(StrokeStyle::new(color(176, 236, 206), 1.0)),
9806 },
9807 ScenePrimitive::Line {
9808 from: UiPoint::new(188.0, 18.0),
9809 to: UiPoint::new(238.0, 52.0),
9810 stroke: StrokeStyle::new(color(232, 186, 88), 3.0),
9811 },
9812 ],
9813 widgets::SceneOptions::default()
9814 .with_layout(LayoutStyle::new().with_width(260.0).with_height(70.0))
9815 .accessibility_label("Scene primitives"),
9816 );
9817
9818 widgets::scroll_container(
9819 ui,
9820 body,
9821 "containers.scroll_area_with_bars",
9822 state.containers_scroll,
9823 widgets::ScrollContainerOptions::default()
9824 .with_axes(ScrollAxes::VERTICAL)
9825 .with_layout(LayoutStyle::column().with_width(260.0).with_height(116.0))
9826 .with_viewport_layout(
9827 LayoutStyle::column()
9828 .with_width(0.0)
9829 .with_height_percent(1.0)
9830 .with_flex_grow(1.0)
9831 .with_flex_shrink(1.0),
9832 ),
9833 |ui, viewport| {
9834 for index in 0..5 {
9835 widgets::label(
9836 ui,
9837 viewport,
9838 format!("containers.scroll_area_with_bars.row.{index}"),
9839 format!("Scrollable row {}", index + 1),
9840 text(12.0, color(200, 212, 228)),
9841 LayoutStyle::new()
9842 .with_width(232.0)
9843 .with_height(28.0)
9844 .with_flex_shrink(0.0),
9845 );
9846 }
9847 },
9848 );
9849
9850 widgets::label(
9851 ui,
9852 body,
9853 "containers.area.title",
9854 "Absolute area",
9855 text(12.0, color(166, 176, 190)),
9856 LayoutStyle::new().with_width_percent(1.0),
9857 );
9858 let area_host = ui.add_child(
9859 body,
9860 UiNode::container(
9861 "containers.area.host",
9862 LayoutStyle::new()
9863 .with_width_percent(1.0)
9864 .with_height(82.0)
9865 .with_flex_shrink(0.0),
9866 )
9867 .with_visual(UiVisual::panel(
9868 color(17, 20, 25),
9869 Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
9870 4.0,
9871 )),
9872 );
9873 widgets::container::area(
9874 ui,
9875 area_host,
9876 "containers.area",
9877 widgets::container::AreaOptions::new(UiRect::new(14.0, 14.0, 180.0, 44.0))
9878 .with_visual(UiVisual::panel(color(39, 72, 109), None, 4.0))
9879 .accessibility_label("Absolute positioned area"),
9880 |ui, area| {
9881 widgets::label(
9882 ui,
9883 area,
9884 "containers.area.label",
9885 "Area",
9886 text(12.0, color(238, 244, 252)),
9887 LayoutStyle::new().with_width_percent(1.0),
9888 );
9889 },
9890 );
9891}
9892
9893fn panel_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9894 let body = section_with_min_viewport(ui, parent, "panels", "Panels", UiSize::new(520.0, 320.0));
9895 widgets::label(
9896 ui,
9897 body,
9898 "panels.title",
9899 "Drag the split bars to resize the docked panels.",
9900 text(12.0, color(166, 176, 190)),
9901 LayoutStyle::new().with_width_percent(1.0),
9902 );
9903 let shell = widgets::frame(
9904 ui,
9905 body,
9906 "panels.shell",
9907 widgets::FrameOptions::default().with_layout(
9908 LayoutStyle::column()
9909 .with_width_percent(1.0)
9910 .with_height(260.0)
9911 .with_flex_grow(1.0)
9912 .with_padding(0.0)
9913 .with_gap(0.0),
9914 ),
9915 );
9916 ext_widgets::split_pane(
9917 ui,
9918 shell,
9919 "panels.top_split",
9920 ext_widgets::SplitAxis::Vertical,
9921 state.panels_top_split,
9922 panel_split_options("panels.resize.top"),
9923 |ui, top| {
9924 panel_region(
9925 ui,
9926 top,
9927 "panels.top",
9928 widgets::PanelKind::Top,
9929 "Top",
9930 "Header controls",
9931 );
9932 },
9933 |ui, lower| {
9934 ext_widgets::split_pane(
9935 ui,
9936 lower,
9937 "panels.bottom_split",
9938 ext_widgets::SplitAxis::Vertical,
9939 state.panels_bottom_split,
9940 panel_split_options("panels.resize.bottom"),
9941 |ui, middle| {
9942 ext_widgets::split_pane(
9943 ui,
9944 middle,
9945 "panels.left_split",
9946 ext_widgets::SplitAxis::Horizontal,
9947 state.panels_left_split,
9948 panel_split_options("panels.resize.left"),
9949 |ui, left| {
9950 panel_region(
9951 ui,
9952 left,
9953 "panels.left",
9954 widgets::PanelKind::Left,
9955 "Left",
9956 "Navigation",
9957 );
9958 },
9959 |ui, center_and_right| {
9960 ext_widgets::split_pane(
9961 ui,
9962 center_and_right,
9963 "panels.right_split",
9964 ext_widgets::SplitAxis::Horizontal,
9965 state.panels_right_split,
9966 panel_split_options("panels.resize.right"),
9967 |ui, center| {
9968 panel_region(
9969 ui,
9970 center,
9971 "panels.center",
9972 widgets::PanelKind::Central,
9973 "Central",
9974 "Primary workspace",
9975 );
9976 },
9977 |ui, right| {
9978 panel_region(
9979 ui,
9980 right,
9981 "panels.right",
9982 widgets::PanelKind::Right,
9983 "Right",
9984 "Inspector",
9985 );
9986 },
9987 );
9988 },
9989 );
9990 },
9991 |ui, bottom| {
9992 panel_region(
9993 ui,
9994 bottom,
9995 "panels.bottom",
9996 widgets::PanelKind::Bottom,
9997 "Bottom",
9998 "Status and output",
9999 );
10000 },
10001 );
10002 },
10003 );
10004}
10005
10006fn panel_split_options(action: &'static str) -> ext_widgets::SplitPaneOptions {
10007 let mut options = ext_widgets::SplitPaneOptions::default().with_handle_action(action);
10008 options.handle_thickness = PANELS_SPLIT_HANDLE_THICKNESS;
10009 options.handle_visual = UiVisual::panel(color(58, 70, 88), None, 0.0);
10010 options.handle_hover_visual = Some(UiVisual::panel(color(100, 172, 244), None, 0.0));
10011 options
10012}
10013
10014fn panel_region(
10015 ui: &mut UiDocument,
10016 parent: UiNodeId,
10017 name: &'static str,
10018 kind: widgets::PanelKind,
10019 title: &'static str,
10020 detail: &'static str,
10021) -> UiNodeId {
10022 let panel = widgets::panel(
10023 ui,
10024 parent,
10025 name,
10026 widgets::PanelOptions {
10027 kind,
10028 layout: LayoutStyle::column()
10029 .with_width_percent(1.0)
10030 .with_height_percent(1.0)
10031 .with_padding(10.0)
10032 .with_gap(6.0),
10033 visual: UiVisual::panel(
10034 color(18, 23, 31),
10035 Some(StrokeStyle::new(color(66, 80, 98), 1.0)),
10036 0.0,
10037 ),
10038 accessibility_label: Some(title.to_string()),
10039 ..Default::default()
10040 },
10041 );
10042 widgets::label(
10043 ui,
10044 panel,
10045 format!("{name}.label"),
10046 title,
10047 text(13.0, color(232, 240, 250)),
10048 LayoutStyle::new().with_width_percent(1.0),
10049 );
10050 widgets::label(
10051 ui,
10052 panel,
10053 format!("{name}.detail"),
10054 detail,
10055 text(11.0, color(154, 166, 184)),
10056 LayoutStyle::new().with_width_percent(1.0),
10057 );
10058 panel
10059}
10060
10061fn form_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10062 let body = section_with_min_viewport(ui, parent, "forms", "Forms", UiSize::new(390.0, 0.0));
10063 let section = widgets::form_section(
10064 ui,
10065 body,
10066 "forms.profile",
10067 None::<String>,
10068 widgets::FormSectionOptions::default().with_layout(
10069 LayoutStyle::column()
10070 .with_width_percent(1.0)
10071 .with_padding(12.0)
10072 .with_gap(10.0),
10073 ),
10074 );
10075 profile_form_summary(ui, section.root, state);
10076
10077 let status_row = wrapping_row(ui, section.root, "forms.profile.status_flags", 6.0);
10078 form_status_chip(
10079 ui,
10080 status_row,
10081 "forms.profile.status.dirty",
10082 "dirty",
10083 state.form.dirty,
10084 );
10085 form_status_chip(
10086 ui,
10087 status_row,
10088 "forms.profile.status.pending",
10089 "pending",
10090 state.form.pending,
10091 );
10092 form_status_chip(
10093 ui,
10094 status_row,
10095 "forms.profile.status.submitted",
10096 "submitted",
10097 state.form.submitted,
10098 );
10099
10100 let mut name_options = widgets::FormRowOptions::default().required();
10101 if state.form_name_text.text().trim().is_empty() {
10102 name_options = name_options.invalid("Name is required");
10103 }
10104 let name = widgets::form_row(ui, section.root, "forms.profile.name", name_options);
10105 widgets::field_label(
10106 ui,
10107 name,
10108 "forms.profile.name.label",
10109 "Name",
10110 widgets::FieldLabelOptions::default().required(),
10111 );
10112 form_text_field(
10113 ui,
10114 name,
10115 "forms.profile.name.input",
10116 &state.form_name_text,
10117 FocusedTextInput::FormName,
10118 state,
10119 );
10120 if state.form_name_text.text().trim().is_empty() {
10121 widgets::field_validation_message(
10122 ui,
10123 name,
10124 "forms.profile.name.validation",
10125 ValidationMessage::error("Name is required"),
10126 widgets::ValidationMessageOptions::default(),
10127 );
10128 } else {
10129 widgets::field_help_text(
10130 ui,
10131 name,
10132 "forms.profile.name.help",
10133 "Shown in window titles and project lists.",
10134 widgets::FieldHelpOptions::default(),
10135 );
10136 }
10137
10138 let mut email_options = widgets::FormRowOptions::default().required();
10139 if !profile_email_valid(state.form_email_text.text()) {
10140 email_options = email_options.invalid("Use a complete email address");
10141 }
10142 let email = widgets::form_row(ui, section.root, "forms.profile.email", email_options);
10143 widgets::field_label(
10144 ui,
10145 email,
10146 "forms.profile.email.label",
10147 "Email",
10148 widgets::FieldLabelOptions::default().required(),
10149 );
10150 form_text_field(
10151 ui,
10152 email,
10153 "forms.profile.email.input",
10154 &state.form_email_text,
10155 FocusedTextInput::FormEmail,
10156 state,
10157 );
10158 if profile_email_valid(state.form_email_text.text()) {
10159 widgets::field_help_text(
10160 ui,
10161 email,
10162 "forms.profile.email.help",
10163 "Used for workspace invites and notifications.",
10164 widgets::FieldHelpOptions::default(),
10165 );
10166 } else {
10167 widgets::field_validation_message(
10168 ui,
10169 email,
10170 "forms.profile.email.validation",
10171 ValidationMessage::error("Use a complete email address"),
10172 widgets::ValidationMessageOptions::default(),
10173 );
10174 }
10175
10176 let role = widgets::form_row(
10177 ui,
10178 section.root,
10179 "forms.profile.role",
10180 widgets::FormRowOptions::default(),
10181 );
10182 widgets::field_label(
10183 ui,
10184 role,
10185 "forms.profile.role.label",
10186 "Role",
10187 widgets::FieldLabelOptions::default(),
10188 );
10189 form_text_field(
10190 ui,
10191 role,
10192 "forms.profile.role.input",
10193 &state.form_role_text,
10194 FocusedTextInput::FormRole,
10195 state,
10196 );
10197 widgets::field_validation_message(
10198 ui,
10199 role,
10200 "forms.profile.role.help",
10201 if state.form_role_text.text().trim().is_empty() {
10202 ValidationMessage::warning("Role can be added later")
10203 } else {
10204 ValidationMessage::info(
10205 "Form rows compose labels, controls, help, and validation text.",
10206 )
10207 },
10208 widgets::ValidationMessageOptions::default(),
10209 );
10210
10211 let newsletter = widgets::form_row(
10212 ui,
10213 section.root,
10214 "forms.profile.newsletter",
10215 widgets::FormRowOptions::default().with_accessibility_label("Newsletter preference"),
10216 );
10217 let mut newsletter_options =
10218 widgets::CheckboxOptions::default().with_action("forms.profile.newsletter.toggle");
10219 newsletter_options.layout = LayoutStyle::new().with_width_percent(1.0).with_height(30.0);
10220 newsletter_options.text_style = text(12.0, color(220, 228, 238));
10221 widgets::checkbox(
10222 ui,
10223 newsletter,
10224 "forms.profile.newsletter.input",
10225 "Send release notes",
10226 state.form_newsletter,
10227 newsletter_options,
10228 );
10229 widgets::field_help_text(
10230 ui,
10231 newsletter,
10232 "forms.profile.newsletter.help",
10233 "Checkboxes participate in the same form state as text fields.",
10234 widgets::FieldHelpOptions::default(),
10235 );
10236
10237 widgets::form_error_summary(
10238 ui,
10239 section.root,
10240 "forms.profile.errors",
10241 &state.form,
10242 widgets::FormErrorSummaryOptions::default(),
10243 );
10244 widgets::label(
10245 ui,
10246 section.root,
10247 "forms.profile.action_help",
10248 "Apply changes saves this draft and keeps editing. Submit profile saves and marks it submitted.",
10249 text(11.0, color(154, 166, 184)),
10250 LayoutStyle::new().with_width_percent(1.0),
10251 );
10252 let action_layout = Layout::row()
10253 .size(LayoutSize::new(
10254 LayoutDimension::percent(1.0),
10255 LayoutDimension::Auto,
10256 ))
10257 .gap(LayoutGap::points(8.0, 8.0))
10258 .flex_wrap(LayoutFlexWrap::Wrap)
10259 .to_layout_style();
10260 widgets::form_action_buttons(
10261 ui,
10262 section.root,
10263 "forms.profile.actions",
10264 &state.form,
10265 widgets::FormActionButtonsOptions::default()
10266 .with_layout(action_layout)
10267 .with_labels(widgets::FormActionLabels {
10268 submit: "Submit profile".to_string(),
10269 apply: "Apply changes".to_string(),
10270 cancel: "Cancel".to_string(),
10271 reset: "Reset".to_string(),
10272 })
10273 .include_reset(true)
10274 .with_action_prefix("forms.profile"),
10275 );
10276 widgets::label(
10277 ui,
10278 section.root,
10279 "forms.profile.status",
10280 format!("Status: {}", state.form_status),
10281 text(11.0, color(154, 166, 184)),
10282 LayoutStyle::new().with_width_percent(1.0),
10283 );
10284}
10285
10286fn overlay_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10287 let body =
10288 section_with_min_viewport(ui, parent, "overlays", "Overlays", UiSize::new(420.0, 0.0));
10289 let header = widgets::collapsing_header(
10290 ui,
10291 body,
10292 "overlays.collapsing",
10293 "Collapsing header",
10294 widgets::CollapsingHeaderOptions::default()
10295 .expanded(state.overlay_expanded)
10296 .with_toggle_action("overlays.collapsing.toggle"),
10297 );
10298 if let Some(panel) = header.body {
10299 widgets::label(
10300 ui,
10301 panel,
10302 "overlays.collapsing.body_text",
10303 "Expanded content lives under the header and remains part of normal layout.",
10304 text(12.0, color(196, 210, 230)),
10305 LayoutStyle::new().with_width_percent(1.0),
10306 );
10307 }
10308
10309 let controls = wrapping_row(ui, body, "overlays.controls", 8.0);
10310 let tooltip_visual = button_visual(58, 78, 96);
10311 let mut tooltip_options = widgets::ButtonOptions::new(LayoutStyle::new().with_height(32.0));
10312 tooltip_options.visual = tooltip_visual;
10313 tooltip_options.hovered_visual = Some(readable_button_hover_visual(tooltip_visual));
10314 tooltip_options.pressed_visual = Some(adjusted_button_visual(tooltip_visual, -62));
10315 tooltip_options.pressed_hovered_visual = Some(adjusted_button_visual(tooltip_visual, -24));
10316 tooltip_options.text_style = text(13.0, color(246, 249, 252));
10317 let tooltip_target = widgets::button(
10318 ui,
10319 controls,
10320 "overlays.tooltip_target",
10321 "Tooltip target",
10322 tooltip_options,
10323 );
10324 ui.node_mut(tooltip_target).set_tooltip(
10325 TooltipContent::new("Tooltip")
10326 .body("Tooltips render as overlay surfaces anchored to a target.")
10327 .shortcut_label("Ctrl+K")
10328 .disabled_reason("Disabled reasons can be announced without changing the trigger."),
10329 );
10330 ui.node_mut(tooltip_target)
10331 .set_tooltip_placement(TooltipPlacement::Below);
10332 ui.node_mut(tooltip_target)
10333 .set_tooltip_size(UiSize::new(240.0, 148.0));
10334 button(
10335 ui,
10336 controls,
10337 "overlays.popup.toggle",
10338 if state.overlay_popup_open {
10339 "Close popup"
10340 } else {
10341 "Open popup"
10342 },
10343 "overlays.popup.toggle",
10344 button_visual(48, 112, 184),
10345 );
10346 button(
10347 ui,
10348 controls,
10349 "overlays.modal.open",
10350 "Open modal",
10351 "overlays.modal.open",
10352 button_visual(58, 78, 96),
10353 );
10354
10355 widgets::label(
10356 ui,
10357 body,
10358 "overlays.tooltip_rect.label",
10359 "A right-edge target keeps its tooltip inside the preview.",
10360 text(12.0, color(166, 176, 190)),
10361 LayoutStyle::new().with_width_percent(1.0),
10362 );
10363 let preview_viewport = UiRect::new(0.0, 0.0, 420.0, 112.0);
10364 let tooltip_target = UiRect::new(328.0, 42.0, 64.0, 28.0);
10365 let tooltip_size = UiSize::new(176.0, 58.0);
10366 let placed_tooltip = widgets::tooltip::tooltip_rect(
10367 tooltip_target,
10368 tooltip_size,
10369 preview_viewport,
10370 TooltipPlacement::Right,
10371 8.0,
10372 None,
10373 );
10374 let clamped_preview = ui.add_child(
10375 body,
10376 UiNode::container(
10377 "overlays.tooltip_rect.preview",
10378 LayoutStyle::new()
10379 .with_width_percent(1.0)
10380 .with_height(112.0)
10381 .with_flex_shrink(0.0),
10382 )
10383 .with_visual(UiVisual::panel(
10384 color(12, 16, 22),
10385 Some(StrokeStyle::new(color(52, 64, 80), 1.0)),
10386 4.0,
10387 )),
10388 );
10389 ui.add_child(
10390 clamped_preview,
10391 UiNode::scene(
10392 "overlays.tooltip_rect.scene",
10393 vec![
10394 ScenePrimitive::Line {
10395 from: UiPoint::new(placed_tooltip.right() + 2.0, placed_tooltip.y + 29.0),
10396 to: UiPoint::new(tooltip_target.x - 2.0, tooltip_target.y + 14.0),
10397 stroke: StrokeStyle::new(color(92, 106, 128), 1.0),
10398 },
10399 ScenePrimitive::Rect(
10400 PaintRect::solid(placed_tooltip, color(24, 29, 38))
10401 .stroke(AlignedStroke::inside(StrokeStyle::new(
10402 color(92, 106, 128),
10403 1.0,
10404 )))
10405 .corner_radii(CornerRadii::uniform(4.0)),
10406 ),
10407 ScenePrimitive::Text(
10408 PaintText::new(
10409 "Tooltip",
10410 UiRect::new(
10411 placed_tooltip.x + 12.0,
10412 placed_tooltip.y + 9.0,
10413 placed_tooltip.width - 24.0,
10414 18.0,
10415 ),
10416 text(12.0, color(225, 233, 244)),
10417 )
10418 .multiline(false),
10419 ),
10420 ScenePrimitive::Text(
10421 PaintText::new(
10422 "Placed inside",
10423 UiRect::new(
10424 placed_tooltip.x + 12.0,
10425 placed_tooltip.y + 31.0,
10426 placed_tooltip.width - 24.0,
10427 18.0,
10428 ),
10429 text(10.0, color(156, 170, 190)),
10430 )
10431 .multiline(false),
10432 ),
10433 ScenePrimitive::Rect(
10434 PaintRect::solid(tooltip_target, color(48, 112, 184))
10435 .stroke(AlignedStroke::inside(StrokeStyle::new(
10436 color(132, 190, 255),
10437 1.0,
10438 )))
10439 .corner_radii(CornerRadii::uniform(3.0)),
10440 ),
10441 ScenePrimitive::Text(
10442 PaintText::new("Target", tooltip_target, text(10.0, color(240, 247, 255)))
10443 .horizontal_align(TextHorizontalAlign::Center)
10444 .vertical_align(TextVerticalAlign::Center)
10445 .multiline(false),
10446 ),
10447 ],
10448 LayoutStyle::new()
10449 .with_width_percent(1.0)
10450 .with_height_percent(1.0),
10451 ),
10452 );
10453
10454 widgets::label(
10455 ui,
10456 body,
10457 "overlays.popup.label",
10458 "Popup panel",
10459 text(12.0, color(166, 176, 190)),
10460 LayoutStyle::new().with_width_percent(1.0),
10461 );
10462 widgets::label(
10463 ui,
10464 body,
10465 "overlays.popup.status",
10466 if state.overlay_popup_open {
10467 "Popup overlay is open."
10468 } else {
10469 "Popup overlay is closed."
10470 },
10471 text(12.0, color(196, 210, 230)),
10472 LayoutStyle::new().with_width_percent(1.0),
10473 );
10474 let popup_host_layout = if state.overlay_popup_open {
10475 LayoutStyle::column()
10476 .with_width_percent(1.0)
10477 .with_height(128.0)
10478 .with_flex_shrink(0.0)
10479 } else {
10480 LayoutStyle::column()
10481 .with_width_percent(1.0)
10482 .with_padding(10.0)
10483 .with_flex_shrink(0.0)
10484 };
10485 let popup_host = ui.add_child(
10486 body,
10487 UiNode::container("overlays.popup.host", popup_host_layout).with_visual(UiVisual::panel(
10488 color(12, 16, 22),
10489 Some(StrokeStyle::new(color(52, 64, 80), 1.0)),
10490 4.0,
10491 )),
10492 );
10493 if state.overlay_popup_open {
10494 let popup = ext_widgets::popup_panel(
10495 ui,
10496 popup_host,
10497 "overlays.popup_panel",
10498 UiRect::new(10.0, 10.0, 220.0, 104.0),
10499 ext_widgets::PopupOptions {
10500 z_index: 4,
10501 portal: UiPortalTarget::Parent,
10502 accessibility: Some(
10503 AccessibilityMeta::new(AccessibilityRole::Dialog).label("Popup preview"),
10504 ),
10505 ..Default::default()
10506 },
10507 );
10508 let popup_body = ui.add_child(
10509 popup,
10510 UiNode::container(
10511 "overlays.popup_panel.body",
10512 LayoutStyle::column()
10513 .with_width_percent(1.0)
10514 .with_height_percent(1.0)
10515 .with_padding(10.0)
10516 .with_gap(6.0),
10517 ),
10518 );
10519 let popup_header = row(ui, popup_body, "overlays.popup_panel.header", 8.0);
10520 widgets::label(
10521 ui,
10522 popup_header,
10523 "overlays.popup_panel.label",
10524 "Popup panel",
10525 text(12.0, color(220, 228, 238)),
10526 LayoutStyle::new().with_width_percent(1.0),
10527 );
10528 let mut close = widgets::ButtonOptions::new(LayoutStyle::size(26.0, 22.0))
10529 .with_action("overlays.popup.close");
10530 close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
10531 close.hovered_visual = Some(button_visual(54, 70, 92));
10532 close.text_style = text(12.0, color(220, 228, 238));
10533 widgets::button(ui, popup_header, "overlays.popup_panel.close", "x", close);
10534 widgets::label(
10535 ui,
10536 popup_body,
10537 "overlays.popup_panel.body_text",
10538 "Popup content is conditionally rendered.",
10539 text(11.0, color(196, 210, 230)),
10540 LayoutStyle::new().with_width_percent(1.0),
10541 );
10542 } else {
10543 widgets::label(
10544 ui,
10545 popup_host,
10546 "overlays.popup.empty",
10547 "Open the popup to render an overlay inside this host.",
10548 text(12.0, color(154, 166, 184)),
10549 LayoutStyle::new().with_width_percent(1.0),
10550 );
10551 }
10552
10553 widgets::label(
10554 ui,
10555 body,
10556 "overlays.toasts.label",
10557 "Toasts",
10558 text(12.0, color(166, 176, 190)),
10559 LayoutStyle::new().with_width_percent(1.0),
10560 );
10561 let toast_controls = row(ui, body, "overlays.toasts.controls", 10.0);
10562 button(
10563 ui,
10564 toast_controls,
10565 "overlays.toasts.show",
10566 "Show toast",
10567 "toast.show",
10568 button_visual(48, 112, 184),
10569 );
10570 button(
10571 ui,
10572 toast_controls,
10573 "overlays.toasts.hide",
10574 "Hide",
10575 "toast.hide",
10576 button_visual(58, 78, 96),
10577 );
10578 widgets::label(
10579 ui,
10580 body,
10581 "overlays.toasts.status",
10582 if state.toast_visible {
10583 "Toast overlay is visible."
10584 } else {
10585 "Toast overlay is hidden."
10586 },
10587 text(12.0, color(196, 210, 230)),
10588 LayoutStyle::new().with_width_percent(1.0),
10589 );
10590 widgets::label(
10591 ui,
10592 body,
10593 "overlays.toasts.action_status",
10594 format!("Action: {}", state.toast_action_status),
10595 text(12.0, color(154, 166, 184)),
10596 LayoutStyle::new().with_width_percent(1.0),
10597 );
10598
10599 if state.overlay_modal_open {
10600 let modal = widgets::modal_dialog(
10601 ui,
10602 parent,
10603 "overlays.modal",
10604 "Modal dialog",
10605 widgets::ModalDialogOptions::default()
10606 .with_size(320.0, 180.0)
10607 .with_close_action("overlays.modal.close")
10608 .with_dismissal(ext_widgets::DialogDismissal::MODAL)
10609 .with_focus_restore(FocusRestoreTarget::Previous),
10610 );
10611 widgets::label(
10612 ui,
10613 modal.body,
10614 "overlays.modal.body.text",
10615 "Modal dialogs are portaled to the application overlay, include a scrim, and trap focus.",
10616 text(12.0, color(220, 228, 238)),
10617 LayoutStyle::new().with_width_percent(1.0),
10618 );
10619 button(
10620 ui,
10621 modal.body,
10622 "overlays.modal.body.close",
10623 "Close modal",
10624 "overlays.modal.close",
10625 button_visual(48, 112, 184),
10626 );
10627 }
10628}
10629
10630fn drag_drop_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10631 let body = section_with_min_viewport(
10632 ui,
10633 parent,
10634 "drag_drop",
10635 "Drag and drop",
10636 UiSize::new(420.0, 0.0),
10637 );
10638 widgets::label(
10639 ui,
10640 body,
10641 "drag_drop.sources.label",
10642 "Drag sources",
10643 text(12.0, color(166, 176, 190)),
10644 LayoutStyle::new().with_width_percent(1.0),
10645 );
10646 let sources = wrapping_row(ui, body, "drag_drop.sources", 8.0);
10647 widgets::dnd_drag_source(
10648 ui,
10649 sources,
10650 "drag_drop.text_source",
10651 "Text payload",
10652 DragPayload::text("Operad payload"),
10653 widgets::DragSourceOptions::default()
10654 .with_layout(drag_source_layout())
10655 .with_kind(DragDropSurfaceKind::ListRow)
10656 .with_allowed_operations([DragOperation::Copy, DragOperation::Move])
10657 .with_action("drag_drop.text_source")
10658 .with_accessibility_hint("Start a text drag operation"),
10659 );
10660 widgets::dnd_drag_source(
10661 ui,
10662 sources,
10663 "drag_drop.file_source",
10664 "File payload",
10665 DragPayload::files(["/tmp/showcase.scene"]),
10666 widgets::DragSourceOptions::default()
10667 .with_layout(drag_source_layout())
10668 .with_kind(DragDropSurfaceKind::Asset)
10669 .with_drag_image_policy(widgets::DragImagePolicy::image_key(
10670 BuiltInIcon::Folder.key(),
10671 UiSize::new(120.0, 36.0),
10672 UiPoint::new(10.0, 10.0),
10673 ))
10674 .with_allowed_operations([DragOperation::Copy])
10675 .with_action("drag_drop.file_source"),
10676 );
10677 widgets::dnd_drag_source(
10678 ui,
10679 sources,
10680 "drag_drop.bytes_source",
10681 "Image bytes",
10682 DragPayload::bytes(DragBytes::new("image/png", vec![137, 80, 78, 71]).name("sprite.png")),
10683 widgets::DragSourceOptions::default()
10684 .with_layout(drag_source_layout())
10685 .with_kind(DragDropSurfaceKind::Asset)
10686 .with_action("drag_drop.bytes_source")
10687 .without_drag_image(),
10688 );
10689
10690 widgets::label(
10691 ui,
10692 body,
10693 "drag_drop.zones.label",
10694 "Drop zones",
10695 text(12.0, color(166, 176, 190)),
10696 LayoutStyle::new().with_width_percent(1.0),
10697 );
10698 let zones = wrapping_row(ui, body, "drag_drop.zones", 8.0);
10699 let accepted_options = widgets::DropZoneOptions::default()
10700 .with_layout(drop_zone_layout())
10701 .with_kind(DragDropSurfaceKind::EditorSurface)
10702 .with_accepted_payload(DropPayloadFilter::empty().text())
10703 .with_accepted_operations([DragOperation::Copy, DragOperation::Move])
10704 .with_action("drag_drop.accept_text")
10705 .with_accessibility_hint("Accepts text payloads");
10706 let accepted = widgets::dnd_drop_zone(
10707 ui,
10708 zones,
10709 "drag_drop.accept_text",
10710 "Text accepted",
10711 accepted_options.clone(),
10712 );
10713 widgets::drag_drop::dnd_apply_drop_zone_preview(
10714 ui,
10715 accepted.root,
10716 &accepted_options,
10717 widgets::drag_drop::DropZonePreviewState::Accepted,
10718 );
10719
10720 let rejected_options = widgets::DropZoneOptions::default()
10721 .with_layout(drop_zone_layout())
10722 .with_kind(DragDropSurfaceKind::Asset)
10723 .with_accepted_payload(DropPayloadFilter::empty().files())
10724 .with_action("drag_drop.files_only");
10725 let rejected = widgets::dnd_drop_zone(
10726 ui,
10727 zones,
10728 "drag_drop.files_only",
10729 "Files only",
10730 rejected_options.clone(),
10731 );
10732 widgets::drag_drop::dnd_apply_drop_zone_preview(
10733 ui,
10734 rejected.root,
10735 &rejected_options,
10736 widgets::drag_drop::DropZonePreviewState::Rejected,
10737 );
10738 let image_options = widgets::DropZoneOptions::default()
10739 .with_layout(drop_zone_layout())
10740 .with_kind(DragDropSurfaceKind::Asset)
10741 .with_accepted_payload(DropPayloadFilter::empty().mime_type("image/*"))
10742 .with_accepted_operations([DragOperation::Copy])
10743 .with_action("drag_drop.image_bytes");
10744 let image_zone = widgets::dnd_drop_zone(
10745 ui,
10746 zones,
10747 "drag_drop.image_bytes",
10748 "Image bytes",
10749 image_options.clone(),
10750 );
10751 widgets::drag_drop::dnd_apply_drop_zone_preview(
10752 ui,
10753 image_zone.root,
10754 &image_options,
10755 widgets::drag_drop::DropZonePreviewState::Hovered,
10756 );
10757
10758 let disabled_options = widgets::DropZoneOptions::default()
10759 .with_layout(drop_zone_layout())
10760 .with_kind(DragDropSurfaceKind::EditorSurface)
10761 .with_accepted_payload(DropPayloadFilter::any())
10762 .with_action("drag_drop.disabled")
10763 .disabled();
10764 let disabled_zone = widgets::dnd_drop_zone(
10765 ui,
10766 zones,
10767 "drag_drop.disabled",
10768 "Disabled",
10769 disabled_options.clone(),
10770 );
10771 widgets::drag_drop::dnd_apply_drop_zone_preview(
10772 ui,
10773 disabled_zone.root,
10774 &disabled_options,
10775 widgets::drag_drop::DropZonePreviewState::Disabled,
10776 );
10777
10778 let operation_row = wrapping_row(ui, body, "drag_drop.operations", 6.0);
10779 dnd_operation_chip(ui, operation_row, "drag_drop.operation.copy", "copy");
10780 dnd_operation_chip(ui, operation_row, "drag_drop.operation.move", "move");
10781 dnd_operation_chip(ui, operation_row, "drag_drop.operation.link", "link");
10782 widgets::label(
10783 ui,
10784 body,
10785 "drag_drop.status",
10786 format!("Status: {}", state.drag_drop_status),
10787 text(11.0, color(154, 166, 184)),
10788 LayoutStyle::new().with_width_percent(1.0),
10789 );
10790}
10791
10792fn media_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10793 let body = section_with_min_viewport(
10794 ui,
10795 parent,
10796 "media",
10797 "Media",
10798 UiSize::new(MEDIA_ICON_TILE_WIDTH, 0.0),
10799 );
10800 widgets::label(
10801 ui,
10802 body,
10803 "media.icons.label",
10804 "Built-in icons",
10805 text(12.0, color(166, 176, 190)),
10806 LayoutStyle::new().with_width_percent(1.0),
10807 );
10808 let icon_columns = media_icon_columns(state);
10809 let icons = media_icon_grid(
10810 ui,
10811 body,
10812 "media.icons",
10813 icon_columns,
10814 BuiltInIcon::COMMON.len(),
10815 );
10816 for icon in BuiltInIcon::COMMON {
10817 media_icon_tile(ui, icons, icon);
10818 }
10819
10820 widgets::label(
10821 ui,
10822 body,
10823 "media.variants.label",
10824 "Image variants",
10825 text(12.0, color(166, 176, 190)),
10826 LayoutStyle::new().with_width_percent(1.0),
10827 );
10828 let variants = wrapping_row(ui, body, "media.variants", 10.0);
10829 widgets::image(
10830 ui,
10831 variants,
10832 "media.image.user_png",
10833 ImageContent::from(ImageHandle::app(SHOWCASE_USER_IMAGE_KEY)),
10834 widgets::ImageOptions::default()
10835 .with_layout(media_preview_image_layout())
10836 .with_accessibility_label("User supplied PNG image"),
10837 );
10838 widgets::image(
10839 ui,
10840 variants,
10841 "media.image.untinted",
10842 icon_image(BuiltInIcon::Play),
10843 widgets::ImageOptions::default()
10844 .with_layout(media_preview_image_layout())
10845 .with_accessibility_label("Untinted play icon"),
10846 );
10847 widgets::image(
10848 ui,
10849 variants,
10850 "media.image.warning",
10851 ImageContent::new(BuiltInIcon::Warning.key()).tinted(color(232, 186, 88)),
10852 widgets::ImageOptions::default()
10853 .with_layout(media_preview_image_layout())
10854 .with_accessibility_label("Tinted warning icon"),
10855 );
10856 widgets::image(
10857 ui,
10858 variants,
10859 "media.image.shader",
10860 ImageContent::new(BuiltInIcon::Grid.key()).tinted(color(118, 183, 255)),
10861 widgets::ImageOptions::default()
10862 .with_layout(media_preview_image_layout())
10863 .with_shader(ShaderEffect::tint(color(169, 119, 255), 0.65))
10864 .with_accessibility_label("Shader-decorated grid icon"),
10865 );
10866}
10867
10868fn shader_effect_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10869 let body = section_with_min_viewport(
10870 ui,
10871 parent,
10872 "shaders",
10873 "Shader effects",
10874 UiSize::new(420.0, 280.0),
10875 );
10876 widgets::label(
10877 ui,
10878 body,
10879 "shaders.effects.label",
10880 "Built-in effects",
10881 text(12.0, color(166, 176, 190)),
10882 LayoutStyle::new().with_width_percent(1.0),
10883 );
10884 let phase = (state.progress_phase / std::f32::consts::TAU).fract();
10885 let previews = wrapping_row(ui, body, "shaders.effects", 10.0);
10886 shader_effect_preview_card(ui, previews, "base", "Base", None);
10887 shader_effect_preview_card(
10888 ui,
10889 previews,
10890 "tint",
10891 "Tint",
10892 Some(ShaderEffect::tint(color(252, 186, 90), 0.72)),
10893 );
10894 shader_effect_preview_card(
10895 ui,
10896 previews,
10897 "shine",
10898 "Shine",
10899 Some(ShaderEffect::shine(phase, 0.55).uniform("width", 0.14)),
10900 );
10901 shader_effect_preview_card(
10902 ui,
10903 previews,
10904 "glow",
10905 "Glow",
10906 Some(ShaderEffect::glow(color(118, 183, 255), 0.9, 7.0)),
10907 );
10908
10909 widgets::label(
10910 ui,
10911 body,
10912 "shaders.widgets.label",
10913 "Applied to widgets",
10914 text(12.0, color(166, 176, 190)),
10915 LayoutStyle::new().with_width_percent(1.0),
10916 );
10917 let panel = ui.add_child(
10918 body,
10919 UiNode::container(
10920 "shaders.widgets",
10921 LayoutStyle::column()
10922 .with_width_percent(1.0)
10923 .with_padding(10.0)
10924 .with_gap(10.0)
10925 .with_flex_shrink(0.0),
10926 )
10927 .with_visual(UiVisual::panel(
10928 color(13, 18, 25),
10929 Some(StrokeStyle::new(color(48, 61, 78), 1.0)),
10930 4.0,
10931 )),
10932 );
10933 let control_row = wrapping_row(ui, panel, "shaders.widgets.controls", 10.0);
10934 let mut shine_button = widgets::ButtonOptions::new(
10935 LayoutStyle::new()
10936 .with_width(150.0)
10937 .with_height(34.0)
10938 .with_flex_shrink(0.0),
10939 );
10940 shine_button.leading_image = Some(icon_image(BuiltInIcon::Settings));
10941 shine_button.image_shader = Some(ShaderEffect::tint(color(111, 203, 159), 0.85));
10942 shine_button.shader = Some(ShaderEffect::shine(phase, 0.45).uniform("width", 0.12));
10943 widgets::button(
10944 ui,
10945 control_row,
10946 "shaders.widgets.button",
10947 "Shine button",
10948 shine_button,
10949 );
10950
10951 widgets::checkbox_with_state(
10952 ui,
10953 control_row,
10954 "shaders.widgets.checkbox",
10955 "Glow check",
10956 widgets::CheckboxState::Checked,
10957 widgets::CheckboxOptions::default()
10958 .with_check_color(color(118, 183, 255))
10959 .with_check_shader(ShaderEffect::glow(color(118, 183, 255), 1.0, 4.0)),
10960 );
10961
10962 let progress_value = smooth_loop(state.progress_phase * 0.8, 0.2) * 100.0;
10963 let mut progress = ext_widgets::ProgressIndicatorOptions::default();
10964 progress.layout = LayoutStyle::new().with_width_percent(1.0).with_height(12.0);
10965 progress.fill_visual = UiVisual::panel(color(111, 203, 159), None, 4.0);
10966 progress.fill_shader = Some(ShaderEffect::shine(phase, 0.5).uniform("width", 0.16));
10967 progress.accessibility_label = Some("Shadered progress fill".to_string());
10968 ext_widgets::progress_indicator(
10969 ui,
10970 panel,
10971 "shaders.widgets.progress",
10972 ext_widgets::ProgressIndicatorValue::percent(progress_value),
10973 progress,
10974 );
10975
10976 let mut slider_options = widgets::SliderOptions::default();
10977 slider_options.layout = LayoutStyle::new()
10978 .with_width_percent(1.0)
10979 .with_height(28.0)
10980 .with_flex_shrink(0.0);
10981 slider_options.fill_shader = Some(ShaderEffect::tint(color(169, 119, 255), 0.55));
10982 slider_options.thumb_shader = Some(ShaderEffect::glow(color(252, 186, 90), 0.9, 4.0));
10983 slider_options.accessibility_label = Some("Shadered slider".to_string());
10984 widgets::slider(
10985 ui,
10986 panel,
10987 "shaders.widgets.slider",
10988 smooth_loop(state.progress_phase * 0.6, 0.4),
10989 0.0..1.0,
10990 slider_options,
10991 );
10992}
10993
10994fn shader_effect_preview_card(
10995 ui: &mut UiDocument,
10996 parent: UiNodeId,
10997 name: &'static str,
10998 label: &'static str,
10999 shader: Option<ShaderEffect>,
11000) {
11001 let tile = ui.add_child(
11002 parent,
11003 UiNode::container(
11004 format!("shaders.effect_tile.{name}"),
11005 LayoutStyle::column()
11006 .with_width(96.0)
11007 .with_height(104.0)
11008 .with_padding(8.0)
11009 .with_gap(8.0)
11010 .with_flex_shrink(0.0),
11011 )
11012 .with_visual(UiVisual::panel(
11013 color(17, 22, 30),
11014 Some(StrokeStyle::new(color(50, 62, 78), 1.0)),
11015 4.0,
11016 ))
11017 .with_accessibility(AccessibilityMeta::new(AccessibilityRole::Group).label(label)),
11018 );
11019 let mut swatch = UiNode::container(
11020 format!("shaders.effect.{name}.swatch"),
11021 LayoutStyle::new()
11022 .with_width_percent(1.0)
11023 .with_height(50.0)
11024 .with_flex_shrink(0.0),
11025 )
11026 .with_visual(UiVisual::panel(
11027 color(64, 109, 194),
11028 Some(StrokeStyle::new(color(138, 164, 194), 1.0)),
11029 8.0,
11030 ));
11031 if let Some(shader) = shader {
11032 swatch = swatch.with_shader(shader);
11033 }
11034 ui.add_child(tile, swatch);
11035 widgets::label(
11036 ui,
11037 tile,
11038 format!("shaders.effect.{name}.label"),
11039 label,
11040 text(11.0, color(204, 216, 232)),
11041 LayoutStyle::new().with_width_percent(1.0),
11042 );
11043}
11044
11045fn shader_lab_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
11046 let body = section_with_min_viewport(
11047 ui,
11048 parent,
11049 "shader_lab",
11050 "Shader lab",
11051 UiSize::new(SHADER_LAB_CONTENT_MIN_WIDTH, SHADER_LAB_CONTENT_MIN_HEIGHT),
11052 );
11053 let source_error = state.shader_lab_source_error.as_deref();
11054 let source_valid = source_error.is_none();
11055
11056 let mut split_options = ext_widgets::SplitPaneOptions::default()
11057 .with_handle_action("shader_lab.workspace.resize")
11058 .with_handle_hover_visual(UiVisual::panel(color(96, 166, 238), None, 2.0));
11059 split_options.layout = Layout::row()
11060 .size(LayoutSize::new(
11061 LayoutDimension::percent(1.0),
11062 LayoutDimension::points(SHADER_LAB_WORKSPACE_HEIGHT),
11063 ))
11064 .min_size(LayoutSize::points(
11065 SHADER_LAB_CONTENT_MIN_WIDTH,
11066 SHADER_LAB_WORKSPACE_HEIGHT,
11067 ))
11068 .flex(0.0, 0.0, LayoutDimension::Auto)
11069 .to_layout_style();
11070 split_options.handle_thickness = SHADER_LAB_SPLIT_HANDLE_THICKNESS;
11071 split_options.handle_visual = UiVisual::panel(color(48, 61, 78), None, 2.0);
11072
11073 ext_widgets::split_pane(
11074 ui,
11075 body,
11076 "shader_lab.workspace",
11077 ext_widgets::SplitAxis::Horizontal,
11078 state.shader_lab_split,
11079 split_options,
11080 |ui, preview_pane| {
11081 shader_lab_preview_column(ui, preview_pane, state, source_error, source_valid);
11082 },
11083 |ui, editor_pane| {
11084 shader_lab_editor_column(ui, editor_pane, state, source_error);
11085 },
11086 );
11087}
11088
11089fn shader_lab_preview_column(
11090 ui: &mut UiDocument,
11091 parent: UiNodeId,
11092 state: &ShowcaseState,
11093 source_error: Option<&str>,
11094 source_valid: bool,
11095) {
11096 let preview_column = ui.add_child(
11097 parent,
11098 UiNode::container(
11099 "shader_lab.preview.column",
11100 LayoutStyle::column()
11101 .with_width_percent(1.0)
11102 .with_height_percent(1.0)
11103 .with_gap(8.0)
11104 .with_flex_shrink(1.0),
11105 ),
11106 );
11107 let target_row = row(ui, preview_column, "shader_lab.target.row", 8.0);
11108 widgets::label(
11109 ui,
11110 target_row,
11111 "shader_lab.target.caption",
11112 "Preview",
11113 text(12.0, color(166, 176, 190)),
11114 LayoutStyle::new()
11115 .with_width(58.0)
11116 .with_height(30.0)
11117 .with_flex_shrink(0.0),
11118 );
11119 shader_lab_dropdown_select(
11120 ui,
11121 target_row,
11122 "shader_lab.target",
11123 &shader_lab_target_options(),
11124 &state.shader_lab_target_menu,
11125 160.0,
11126 "Preview target",
11127 "Shader lab preview target",
11128 );
11129 shader_lab_preview_controls(ui, preview_column, state);
11130
11131 let preview = ui.add_child(
11132 preview_column,
11133 UiNode::container(
11134 "shader_lab.preview.surface",
11135 LayoutStyle::column()
11136 .with_width_percent(1.0)
11137 .with_height(0.0)
11138 .with_flex_grow(1.0)
11139 .with_padding(18.0)
11140 .with_gap(8.0)
11141 .with_align_items(taffy::prelude::AlignItems::Center)
11142 .with_justify_content(taffy::prelude::JustifyContent::Center)
11143 .with_flex_shrink(0.0),
11144 )
11145 .with_visual(UiVisual::panel(
11146 color(8, 12, 18),
11147 Some(StrokeStyle::new(color(48, 61, 78), 1.0)),
11148 4.0,
11149 )),
11150 );
11151 shader_lab_preview(ui, preview, state, source_valid);
11152
11153 if let Some(status) = shader_lab_status_label(state, source_error) {
11154 widgets::label(
11155 ui,
11156 preview_column,
11157 "shader_lab.preview.status",
11158 status,
11159 text(11.0, color(166, 176, 190)),
11160 LayoutStyle::new().with_width_percent(1.0),
11161 );
11162 }
11163 shader_lab_material_contract_demo(ui, preview_column, state);
11164}
11165
11166fn shader_lab_preview_controls(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
11167 let controls = ui.add_child(
11168 parent,
11169 UiNode::container(
11170 "shader_lab.preview.controls",
11171 LayoutStyle::column()
11172 .with_width_percent(1.0)
11173 .with_gap(6.0)
11174 .with_flex_shrink(0.0),
11175 ),
11176 );
11177 let text_row = wrapping_row(ui, controls, "shader_lab.preview.text_controls", 8.0);
11178 shader_lab_option_checkbox(
11179 ui,
11180 text_row,
11181 "shader_lab.frame_text.toggle",
11182 "Frame",
11183 state.shader_lab_show_frame_text,
11184 );
11185 shader_lab_option_checkbox(
11186 ui,
11187 text_row,
11188 "shader_lab.button_text.toggle",
11189 "Button",
11190 state.shader_lab_show_button_text,
11191 );
11192
11193 let style_row = wrapping_row(ui, controls, "shader_lab.preview.style_controls", 8.0);
11194 shader_lab_slider_control(
11195 ui,
11196 style_row,
11197 "shader_lab.surface.stroke",
11198 "Border",
11199 state.shader_lab_surface_stroke_width,
11200 SHADER_LAB_SURFACE_STROKE_MAX,
11201 1,
11202 );
11203 shader_lab_slider_control(
11204 ui,
11205 style_row,
11206 "shader_lab.surface.radius",
11207 "Radius",
11208 state.shader_lab_surface_radius,
11209 SHADER_LAB_SURFACE_RADIUS_MAX,
11210 0,
11211 );
11212}
11213
11214fn shader_lab_material_contract_demo(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
11215 let panel = ui.add_child(
11216 parent,
11217 UiNode::container(
11218 "shader_lab.material",
11219 LayoutStyle::column()
11220 .with_width_percent(1.0)
11221 .with_gap(6.0)
11222 .with_flex_shrink(0.0),
11223 ),
11224 );
11225 widgets::label(
11226 ui,
11227 panel,
11228 "shader_lab.material.title",
11229 "Material",
11230 text(12.0, color(186, 198, 216)),
11231 LayoutStyle::new().with_width_percent(1.0),
11232 );
11233 let controls = wrapping_row(ui, panel, "shader_lab.material.controls", 8.0);
11234 shader_lab_labeled_dropdown(
11235 ui,
11236 controls,
11237 "shader_lab.material.shader",
11238 "Shader",
11239 &shader_lab_material_shader_options(),
11240 &state.shader_lab_material_shader_menu,
11241 132.0,
11242 );
11243 shader_lab_labeled_dropdown(
11244 ui,
11245 controls,
11246 "shader_lab.material.shape",
11247 "Shape",
11248 &shader_lab_material_shape_options(),
11249 &state.shader_lab_material_shape_menu,
11250 132.0,
11251 );
11252 shader_lab_labeled_dropdown(
11253 ui,
11254 controls,
11255 "shader_lab.material.geometry",
11256 "Geometry",
11257 &shader_lab_material_geometry_options(),
11258 &state.shader_lab_material_geometry_menu,
11259 140.0,
11260 );
11261 shader_lab_slider_control(
11262 ui,
11263 controls,
11264 "shader_lab.material.outset",
11265 "Outset",
11266 state.shader_lab_material_outset,
11267 SHADER_LAB_MATERIAL_OUTSET_MAX,
11268 0,
11269 );
11270
11271 let contracts_layout = Layout::column()
11272 .size(LayoutSize::new(
11273 LayoutDimension::percent(1.0),
11274 LayoutDimension::Auto,
11275 ))
11276 .padding(LayoutSpacing::new(
11277 LayoutLength::points(4.0),
11278 LayoutLength::points(4.0),
11279 LayoutLength::points(SHADER_LAB_MATERIAL_OUTSET_MAX),
11280 LayoutLength::points(8.0),
11281 ))
11282 .flex(0.0, 0.0, LayoutDimension::Auto)
11283 .to_layout_style();
11284 let contracts_shell = ui.add_child(
11285 panel,
11286 UiNode::container("shader_lab.material.contracts.shell", contracts_layout),
11287 );
11288 let row = wrapping_row(ui, contracts_shell, "shader_lab.material.contracts", 8.0);
11289 shader_lab_material_chip(
11290 ui,
11291 row,
11292 "shader_lab.material.current",
11293 "Selected",
11294 shader_lab_selected_material(state),
11295 shader_lab_material_visual(state.shader_lab_material_shape),
11296 );
11297 shader_lab_material_chip(
11298 ui,
11299 row,
11300 "shader_lab.material.outset",
11301 "Glow",
11302 ElementMaterial::shader(ShaderEffect::glow(
11303 color(100, 180, 255),
11304 0.95,
11305 SHADER_LAB_MATERIAL_OUTSET,
11306 ))
11307 .with_paint_outset(LayoutInsets::points(
11308 state
11309 .shader_lab_material_outset
11310 .clamp(0.0, SHADER_LAB_MATERIAL_OUTSET_MAX),
11311 )),
11312 UiVisual::panel(color(32, 64, 96), None, 8.0),
11313 );
11314 shader_lab_material_chip(
11315 ui,
11316 row,
11317 "shader_lab.material.circle_hit",
11318 "Circle",
11319 ElementMaterial::new()
11320 .with_clip_shape(ElementShape::circle())
11321 .with_hit_shape(ElementShape::circle()),
11322 UiVisual::panel(
11323 color(74, 133, 198),
11324 Some(StrokeStyle::new(color(212, 232, 255), 1.0)),
11325 999.0,
11326 ),
11327 );
11328 shader_lab_material_chip(
11329 ui,
11330 row,
11331 "shader_lab.material.geometry_chip",
11332 "Warp",
11333 ElementMaterial::new()
11334 .with_paint_outset(LayoutInsets::points(8.0))
11335 .with_geometry_effect(GeometryEffect::wave(8.0)),
11336 UiVisual::panel(
11337 color(101, 70, 170),
11338 Some(StrokeStyle::new(color(214, 196, 255), 1.0)),
11339 6.0,
11340 ),
11341 );
11342}
11343
11344fn shader_lab_labeled_dropdown(
11345 ui: &mut UiDocument,
11346 parent: UiNodeId,
11347 name: &'static str,
11348 label: &'static str,
11349 options: &[ext_widgets::SelectOption],
11350 state: &ext_widgets::SelectMenuState,
11351 width: f32,
11352) {
11353 let control = ui.add_child(
11354 parent,
11355 UiNode::container(
11356 format!("{name}.control"),
11357 LayoutStyle::row()
11358 .with_width(width + 74.0)
11359 .with_height(30.0)
11360 .with_gap(6.0)
11361 .with_align_items(taffy::prelude::AlignItems::Center)
11362 .with_flex_shrink(0.0),
11363 ),
11364 );
11365 widgets::label(
11366 ui,
11367 control,
11368 format!("{name}.caption"),
11369 label,
11370 text(12.0, color(166, 176, 190)),
11371 LayoutStyle::new()
11372 .with_width(66.0)
11373 .with_height(30.0)
11374 .with_flex_shrink(0.0),
11375 );
11376 shader_lab_dropdown_select(ui, control, name, options, state, width, label, label);
11377}
11378
11379fn shader_lab_dropdown_select(
11380 ui: &mut UiDocument,
11381 parent: UiNodeId,
11382 name: &'static str,
11383 options: &[ext_widgets::SelectOption],
11384 state: &ext_widgets::SelectMenuState,
11385 width: f32,
11386 placeholder: &'static str,
11387 accessibility_label: &'static str,
11388) {
11389 let anchor = ui.add_child(
11390 parent,
11391 UiNode::container(
11392 format!("{name}.anchor"),
11393 LayoutStyle::new()
11394 .with_width(width)
11395 .with_height(30.0)
11396 .with_flex_shrink(0.0),
11397 ),
11398 );
11399 let nodes = ext_widgets::dropdown_select(
11400 ui,
11401 anchor,
11402 name,
11403 options,
11404 state,
11405 Some(select_popup(
11406 UiRect::new(0.0, 0.0, width, 30.0),
11407 UiRect::new(0.0, 0.0, width + 48.0, 240.0),
11408 )),
11409 dropdown_select_options(width, name, placeholder, accessibility_label),
11410 );
11411 ui.node_mut(nodes.trigger)
11412 .set_action(format!("{name}.toggle"));
11413}
11414
11415fn shader_lab_selected_material(state: &ShowcaseState) -> ElementMaterial {
11416 let shape = state.shader_lab_material_shape.shape();
11417 let mut material = ElementMaterial::new()
11418 .with_paint_outset(LayoutInsets::points(
11419 state
11420 .shader_lab_material_outset
11421 .clamp(0.0, SHADER_LAB_MATERIAL_OUTSET_MAX),
11422 ))
11423 .with_clip_shape(shape.clone())
11424 .with_hit_shape(shape)
11425 .with_geometry_effect(state.shader_lab_material_geometry.effect());
11426 if let Some(shader) = shader_lab_material_shader_effect(state) {
11427 material = material.with_shader(shader);
11428 }
11429 material
11430}
11431
11432fn shader_lab_material_shader_effect(state: &ShowcaseState) -> Option<ShaderEffect> {
11433 let phase = state.progress_phase.rem_euclid(1.0);
11434 match state.shader_lab_material_shader {
11435 ShaderLabMaterialShader::None => None,
11436 ShaderLabMaterialShader::Tint => Some(ShaderEffect::tint(color(255, 196, 92), 0.62)),
11437 ShaderLabMaterialShader::Shine => Some(ShaderEffect::shine(phase, 0.92)),
11438 ShaderLabMaterialShader::Glow => Some(ShaderEffect::glow(
11439 color(100, 180, 255),
11440 0.95,
11441 state
11442 .shader_lab_material_outset
11443 .clamp(0.0, SHADER_LAB_MATERIAL_OUTSET_MAX),
11444 )),
11445 ShaderLabMaterialShader::Plasma => {
11446 Some(ShaderEffect::plasma(phase, color(82, 190, 255), 0.75, 12.0))
11447 }
11448 ShaderLabMaterialShader::Rings => {
11449 Some(ShaderEffect::rings(phase, color(232, 170, 88), 0.78, 11.0))
11450 }
11451 ShaderLabMaterialShader::Grid => {
11452 Some(ShaderEffect::grid(phase, color(156, 132, 255), 0.85, 9.0))
11453 }
11454 }
11455}
11456
11457fn shader_lab_material_visual(shape: ShaderLabMaterialShape) -> UiVisual {
11458 UiVisual::panel(
11459 color(39, 71, 114),
11460 Some(StrokeStyle::new(color(168, 205, 255), 1.0)),
11461 shape.visual_radius(),
11462 )
11463}
11464
11465fn shader_lab_material_chip(
11466 ui: &mut UiDocument,
11467 parent: UiNodeId,
11468 name: &'static str,
11469 label: &'static str,
11470 material: ElementMaterial,
11471 visual: UiVisual,
11472) {
11473 let chip = ui.add_child(
11474 parent,
11475 UiNode::container(
11476 name,
11477 LayoutStyle::row()
11478 .with_width(156.0)
11479 .with_height(44.0)
11480 .with_padding(8.0)
11481 .with_align_items(taffy::prelude::AlignItems::Center)
11482 .with_justify_content(taffy::prelude::JustifyContent::Center)
11483 .with_flex_shrink(0.0),
11484 )
11485 .with_visual(visual)
11486 .with_material(material)
11487 .with_accessibility(
11488 AccessibilityMeta::new(AccessibilityRole::Image).label(format!("{label} material")),
11489 ),
11490 );
11491 widgets::label(
11492 ui,
11493 chip,
11494 format!("{name}.text"),
11495 label,
11496 text(11.0, color(246, 249, 252)),
11497 LayoutStyle::new().with_flex_shrink(0.0),
11498 );
11499}
11500
11501fn shader_lab_option_checkbox(
11502 ui: &mut UiDocument,
11503 parent: UiNodeId,
11504 name: &'static str,
11505 label: &'static str,
11506 checked: bool,
11507) {
11508 let mut options = widgets::CheckboxOptions::default()
11509 .with_action(name)
11510 .with_text_style(text(12.0, color(220, 228, 238)));
11511 options.layout = LayoutStyle::new()
11512 .with_width(112.0)
11513 .with_height(24.0)
11514 .with_flex_shrink(0.0);
11515 widgets::checkbox(ui, parent, name, label, checked, options);
11516}
11517
11518fn shader_lab_slider_control(
11519 ui: &mut UiDocument,
11520 parent: UiNodeId,
11521 name: &'static str,
11522 label: &'static str,
11523 value: f32,
11524 max: f32,
11525 decimals: usize,
11526) {
11527 let control = ui.add_child(
11528 parent,
11529 UiNode::container(
11530 format!("{name}.control"),
11531 Layout::row()
11532 .size(LayoutSize::new(
11533 LayoutDimension::points(214.0),
11534 LayoutDimension::Auto,
11535 ))
11536 .align_items(LayoutAlignment::Center)
11537 .gap(LayoutGap::points(6.0, 6.0))
11538 .flex(0.0, 0.0, LayoutDimension::Auto)
11539 .to_layout_style(),
11540 ),
11541 );
11542 widgets::label(
11543 ui,
11544 control,
11545 format!("{name}.label"),
11546 label,
11547 text(12.0, color(166, 176, 190)),
11548 LayoutStyle::new()
11549 .with_width(46.0)
11550 .with_height(22.0)
11551 .with_flex_shrink(0.0),
11552 );
11553 let mut options = widgets::SliderOptions::default()
11554 .with_layout(
11555 LayoutStyle::new()
11556 .with_width(96.0)
11557 .with_height(22.0)
11558 .with_flex_shrink(0.0),
11559 )
11560 .with_value_edit_action(name);
11561 options.accessibility_label = Some(format!("Shader lab {label}"));
11562 widgets::slider(
11563 ui,
11564 control,
11565 format!("{name}.slider"),
11566 (value / max.max(f32::EPSILON)).clamp(0.0, 1.0),
11567 0.0..1.0,
11568 options,
11569 );
11570 widgets::label(
11571 ui,
11572 control,
11573 format!("{name}.value"),
11574 format!("{value:.decimals$}px"),
11575 text(12.0, color(226, 232, 242)),
11576 LayoutStyle::new()
11577 .with_width(48.0)
11578 .with_height(22.0)
11579 .with_flex_shrink(0.0),
11580 );
11581}
11582
11583fn shader_lab_surface_stroke(state: &ShowcaseState) -> Option<StrokeStyle> {
11584 (state.shader_lab_surface_stroke_width > f32::EPSILON).then(|| {
11585 StrokeStyle::new(
11586 color(150, 180, 235),
11587 state
11588 .shader_lab_surface_stroke_width
11589 .clamp(0.0, SHADER_LAB_SURFACE_STROKE_MAX),
11590 )
11591 })
11592}
11593
11594fn shader_lab_surface_radius(state: &ShowcaseState) -> f32 {
11595 state
11596 .shader_lab_surface_radius
11597 .clamp(0.0, SHADER_LAB_SURFACE_RADIUS_MAX)
11598}
11599
11600fn shader_lab_editor_column(
11601 ui: &mut UiDocument,
11602 parent: UiNodeId,
11603 state: &ShowcaseState,
11604 source_error: Option<&str>,
11605) {
11606 let editor_column = ui.add_child(
11607 parent,
11608 UiNode::container(
11609 "shader_lab.editor.column",
11610 LayoutStyle::column()
11611 .with_width_percent(1.0)
11612 .with_height_percent(1.0)
11613 .with_gap(8.0)
11614 .with_flex_shrink(1.0),
11615 ),
11616 );
11617 let preset_row = row(ui, editor_column, "shader_lab.preset.row", 8.0);
11618 widgets::label(
11619 ui,
11620 preset_row,
11621 "shader_lab.preset.caption",
11622 "Program",
11623 text(12.0, color(166, 176, 190)),
11624 LayoutStyle::new()
11625 .with_width(62.0)
11626 .with_height(30.0)
11627 .with_flex_shrink(0.0),
11628 );
11629 shader_lab_dropdown_select(
11630 ui,
11631 preset_row,
11632 "shader_lab.preset",
11633 &shader_lab_preset_options(),
11634 &state.shader_lab_preset_menu,
11635 180.0,
11636 "WGSL preset",
11637 "Shader lab WGSL preset",
11638 );
11639
11640 let editor_frame = ui.add_child(
11641 editor_column,
11642 UiNode::container(
11643 "shader_lab.editor.frame",
11644 Layout::column()
11645 .size(LayoutSize::new(
11646 LayoutDimension::percent(1.0),
11647 LayoutDimension::points(0.0),
11648 ))
11649 .min_size(LayoutSize::points(0.0, SHADER_LAB_EDITOR_HEIGHT))
11650 .flex(1.0, 1.0, LayoutDimension::points(0.0))
11651 .to_layout_style(),
11652 )
11653 .with_visual(UiVisual::panel(
11654 color(18, 22, 28),
11655 Some(StrokeStyle::new(color(72, 84, 104), 1.0)),
11656 4.0,
11657 )),
11658 );
11659 let editor_scroll = widgets::scroll_area(
11660 ui,
11661 editor_frame,
11662 "shader_lab.editor.scroll",
11663 ScrollAxes::BOTH,
11664 LayoutStyle::column()
11665 .with_width_percent(1.0)
11666 .with_height_percent(1.0),
11667 );
11668 ui.node_mut(editor_scroll)
11669 .set_action("shader_lab.editor.scroll");
11670 if let Some(scroll) = ui.node_mut(editor_scroll).scroll_mut() {
11671 scroll.set_offset(state.shader_lab_editor_scroll);
11672 }
11673
11674 let mut code_options = state.text_edit_options(FocusedTextInput::ShaderLabSource);
11675 code_options.edit_action = Some("shader_lab.editor.edit".into());
11676 code_options.visual = UiVisual::TRANSPARENT;
11677 code_options.focused_visual = Some(UiVisual::TRANSPARENT);
11678 code_options.disabled_visual = Some(UiVisual::TRANSPARENT);
11679 widgets::code_editor(
11680 ui,
11681 editor_scroll,
11682 "shader_lab.editor",
11683 &state.shader_lab_source,
11684 code_options,
11685 );
11686 let (validation_text, validation_color) = shader_lab_validation_label(source_error);
11687 widgets::label(
11688 ui,
11689 editor_column,
11690 "shader_lab.validation",
11691 validation_text,
11692 text(11.0, validation_color),
11693 LayoutStyle::new().with_width_percent(1.0),
11694 );
11695}
11696
11697fn shader_lab_editor_content_size(source: &str) -> UiSize {
11698 let style = widgets::code_text_style();
11699 let line_count = source.lines().count().max(1) as f32;
11700 let longest_line = source
11701 .lines()
11702 .map(|line| line.chars().count())
11703 .max()
11704 .unwrap_or(0)
11705 .max(48) as f32;
11706 UiSize::new(
11707 (longest_line * style.font_size * 0.56 + 24.0).max(SHADER_LAB_EDITOR_WIDTH),
11708 (line_count * style.line_height + 18.0).max(SHADER_LAB_EDITOR_HEIGHT),
11709 )
11710}
11711
11712fn shader_lab_preview(
11713 ui: &mut UiDocument,
11714 parent: UiNodeId,
11715 state: &ShowcaseState,
11716 source_valid: bool,
11717) {
11718 match state.shader_lab_target {
11719 ShaderLabTarget::Canvas => {
11720 let mut options = widgets::CanvasOptions::default()
11721 .with_layout(
11722 LayoutStyle::new()
11723 .with_width_percent(1.0)
11724 .with_height_percent(1.0)
11725 .with_flex_grow(1.0),
11726 )
11727 .with_intrinsic_size(UiSize::new(
11728 SHADER_LAB_PREVIEW_WIDTH - 20.0,
11729 SHADER_LAB_PREVIEW_HEIGHT,
11730 ))
11731 .with_accessibility_label("Shader lab canvas preview");
11732 options.visual = UiVisual::panel(color(8, 12, 18), None, 4.0);
11733 widgets::canvas(
11734 ui,
11735 parent,
11736 "shader_lab.preview.canvas",
11737 CanvasContent::new("shader_lab.preview.canvas")
11738 .program(shader_lab_canvas_program(state, source_valid)),
11739 options,
11740 );
11741 }
11742 ShaderLabTarget::Frame => {
11743 let mut frame_node = UiNode::container(
11744 "shader_lab.preview.frame",
11745 operad::layout::with_min_size(
11746 LayoutStyle::column()
11747 .with_width_percent(0.82)
11748 .with_height_percent(0.62)
11749 .with_padding(14.0)
11750 .with_align_items(taffy::prelude::AlignItems::Center)
11751 .with_justify_content(taffy::prelude::JustifyContent::Center)
11752 .with_flex_shrink(0.0),
11753 operad::layout::px(SHADER_LAB_FRAME_MIN_WIDTH),
11754 operad::layout::px(SHADER_LAB_FRAME_MIN_HEIGHT),
11755 ),
11756 )
11757 .with_visual(UiVisual::panel(
11758 ColorRgba::new(0, 0, 0, 0),
11759 shader_lab_surface_stroke(state),
11760 shader_lab_surface_radius(state),
11761 ));
11762 frame_node.style_mut().set_clip(ClipBehavior::Clip);
11763 let frame = ui.add_child(parent, frame_node);
11764 shader_lab_canvas_layer_fill(
11765 ui,
11766 frame,
11767 "shader_lab.preview.frame.shader",
11768 state,
11769 source_valid,
11770 );
11771 if state.shader_lab_show_frame_text {
11772 let label = widgets::label(
11773 ui,
11774 frame,
11775 "shader_lab.preview.frame.label",
11776 "WGSL frame",
11777 text(14.0, color(246, 249, 252)),
11778 LayoutStyle::new().with_flex_shrink(0.0),
11779 );
11780 ui.node_mut(label).style_mut().set_z_index(1);
11781 }
11782 }
11783 ShaderLabTarget::Button => {
11784 let mut shell_node = UiNode::container(
11785 "shader_lab.preview.button.shell",
11786 LayoutStyle::column()
11787 .with_width(SHADER_LAB_BUTTON_WIDTH)
11788 .with_height(SHADER_LAB_BUTTON_HEIGHT)
11789 .with_align_items(taffy::prelude::AlignItems::Center)
11790 .with_justify_content(taffy::prelude::JustifyContent::Center),
11791 )
11792 .with_visual(UiVisual::TRANSPARENT);
11793 shell_node.style_mut().set_clip(ClipBehavior::Clip);
11794 let shell = ui.add_child(parent, shell_node);
11795 shader_lab_canvas_layer_fill(
11796 ui,
11797 shell,
11798 "shader_lab.preview.button.shader",
11799 state,
11800 source_valid,
11801 );
11802 let mut options = widgets::ButtonOptions::new(
11803 LayoutStyle::new()
11804 .with_width(SHADER_LAB_BUTTON_WIDTH)
11805 .with_height(SHADER_LAB_BUTTON_HEIGHT),
11806 )
11807 .with_action("shader_lab.preview.button")
11808 .with_accessibility_label("Shader button preview");
11809 options.visual = UiVisual::panel(
11810 ColorRgba::new(0, 0, 0, 0),
11811 shader_lab_surface_stroke(state),
11812 shader_lab_surface_radius(state),
11813 );
11814 options.hovered_visual = Some(UiVisual::panel(
11815 ColorRgba::new(255, 255, 255, 28),
11816 shader_lab_surface_stroke(state),
11817 shader_lab_surface_radius(state),
11818 ));
11819 options.pressed_visual = Some(UiVisual::panel(
11820 ColorRgba::new(0, 0, 0, 48),
11821 shader_lab_surface_stroke(state),
11822 shader_lab_surface_radius(state),
11823 ));
11824 options.text_style = text(14.0, color(246, 249, 252));
11825 let button = widgets::button(
11826 ui,
11827 shell,
11828 "shader_lab.preview.button",
11829 if state.shader_lab_show_button_text {
11830 "Shader button"
11831 } else {
11832 ""
11833 },
11834 options,
11835 );
11836 ui.node_mut(button).style_mut().set_z_index(1);
11837 }
11838 }
11839}
11840
11841fn shader_lab_canvas_layer_fill(
11842 ui: &mut UiDocument,
11843 parent: UiNodeId,
11844 name: &'static str,
11845 state: &ShowcaseState,
11846 source_valid: bool,
11847) -> UiNodeId {
11848 let layout = operad::layout::with_absolute_position(
11849 LayoutStyle::new()
11850 .with_width_percent(1.0)
11851 .with_height_percent(1.0),
11852 0.0,
11853 0.0,
11854 );
11855 let mut options = widgets::CanvasOptions::default()
11856 .with_layout(layout)
11857 .with_intrinsic_size(UiSize::new(
11858 SHADER_LAB_PREVIEW_WIDTH,
11859 SHADER_LAB_PREVIEW_HEIGHT,
11860 ))
11861 .with_accessibility_label(format!("{name} shader preview"));
11862 options.input = InputBehavior::NONE;
11863 options.visual = UiVisual::TRANSPARENT;
11864 let canvas = widgets::canvas(
11865 ui,
11866 parent,
11867 name,
11868 CanvasContent::new(name).program(shader_lab_canvas_program(state, source_valid)),
11869 options,
11870 );
11871 ui.node_mut(canvas).style_mut().set_z_index(0);
11872 canvas
11873}
11874
11875fn shader_lab_status_label(state: &ShowcaseState, source_error: Option<&str>) -> Option<String> {
11876 if source_error.is_some() {
11877 Some(format!(
11878 "{} fallback until WGSL validates",
11879 state.shader_lab_target.label()
11880 ))
11881 } else {
11882 None
11883 }
11884}
11885
11886fn shader_lab_validation_label(source_error: Option<&str>) -> (String, ColorRgba) {
11887 if let Some(error) = source_error {
11888 (
11889 format!("WGSL error: {}", compact_shader_error(error, 160)),
11890 color(255, 139, 128),
11891 )
11892 } else {
11893 ("WGSL valid".to_string(), color(112, 221, 160))
11894 }
11895}
11896
11897fn compact_shader_error(error: &str, max_chars: usize) -> String {
11898 let mut compact = error.split_whitespace().collect::<Vec<_>>().join(" ");
11899 if compact.chars().count() > max_chars {
11900 compact = compact.chars().take(max_chars.saturating_sub(3)).collect();
11901 compact.push_str("...");
11902 }
11903 compact
11904}
11905
11906fn shader_lab_canvas_program(state: &ShowcaseState, source_valid: bool) -> CanvasRenderProgram {
11907 let source = if source_valid {
11908 state.shader_lab_source.text().to_string()
11909 } else {
11910 SHADER_LAB_ERROR_WGSL.to_string()
11911 };
11912 CanvasRenderProgram::wgsl(source)
11913 .label("showcase.shader_lab.canvas")
11914 .constant("TIME", state.progress_phase as f64)
11915 .clear_color(Some(color(8, 12, 18)))
11916}
11917
11918fn shader_lab_source_error(source: &str) -> Option<String> {
11919 if !shader_lab_source_has_entry_points(source) {
11920 return Some("source must define @vertex fn vs_main and @fragment fn fs_main".to_string());
11921 }
11922
11923 shader_lab_compile_error(source)
11924}
11925
11926#[cfg(feature = "wgpu")]
11927fn shader_lab_compile_error(source: &str) -> Option<String> {
11928 let module = match naga::front::wgsl::parse_str(source) {
11929 Ok(module) => module,
11930 Err(error) => return Some(error.emit_to_string(source)),
11931 };
11932 let mut validator = naga::valid::Validator::new(
11933 naga::valid::ValidationFlags::all(),
11934 naga::valid::Capabilities::empty(),
11935 );
11936 validator
11937 .validate(&module)
11938 .err()
11939 .map(|error| error.to_string())
11940}
11941
11942#[cfg(not(feature = "wgpu"))]
11943fn shader_lab_compile_error(_source: &str) -> Option<String> {
11944 None
11945}
11946
11947fn shader_lab_source_has_entry_points(source: &str) -> bool {
11948 source.contains("@vertex")
11949 && source.contains("fn vs_main")
11950 && source.contains("@fragment")
11951 && source.contains("fn fs_main")
11952}
11953
11954fn timeline_ruler(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
11955 let body =
11956 section_with_min_viewport(ui, parent, "timeline", "Timeline", UiSize::new(560.0, 0.0));
11957 widgets::label(
11958 ui,
11959 body,
11960 "timeline.label",
11961 "Clip timeline",
11962 text(12.0, color(166, 176, 190)),
11963 LayoutStyle::new().with_width_percent(1.0),
11964 );
11965 widgets::label(
11966 ui,
11967 body,
11968 "timeline.description",
11969 "The ruler maps time to tracks, clips, markers, and the current playhead.",
11970 text(12.0, color(196, 210, 230)),
11971 LayoutStyle::new().with_width_percent(1.0),
11972 );
11973
11974 let editor = row(ui, body, "timeline.editor", 0.0);
11975 let labels = ui.add_child(
11976 editor,
11977 UiNode::container(
11978 "timeline.lane_labels",
11979 LayoutStyle::column()
11980 .with_width(96.0)
11981 .with_height(172.0)
11982 .with_flex_shrink(0.0),
11983 ),
11984 );
11985 for (name, label, height) in [
11986 ("timeline.lane_labels.header", "Tracks", 40.0),
11987 ("timeline.lane_labels.video", "Video", 44.0),
11988 ("timeline.lane_labels.audio", "Audio", 44.0),
11989 ("timeline.lane_labels.notes", "Notes", 44.0),
11990 ] {
11991 widgets::label(
11992 ui,
11993 labels,
11994 name,
11995 label,
11996 text(11.0, color(166, 176, 190)),
11997 LayoutStyle::new()
11998 .with_width_percent(1.0)
11999 .with_height(height)
12000 .with_flex_shrink(0.0),
12001 );
12002 }
12003
12004 let timeline_scroll = timeline_scroll_state_for_view(
12005 state.timeline_scroll,
12006 state.timeline_scroll.viewport_size().width,
12007 );
12008 let range = ext_widgets::TimelineRange::new(0.0, 48.0);
12009 let nodes = scroll_area_widgets::scroll_container_shell(
12010 ui,
12011 editor,
12012 "timeline",
12013 timeline_scroll,
12014 widgets::ScrollContainerOptions::default()
12015 .with_axes(ScrollAxes::HORIZONTAL)
12016 .with_action_prefix("timeline")
12017 .with_gap(4.0)
12018 .with_scrollbar_thickness(TIMELINE_SCROLLBAR_HEIGHT)
12019 .with_layout(
12020 LayoutStyle::column()
12021 .with_width(0.0)
12022 .with_flex_grow(1.0)
12023 .with_height(TIMELINE_SCROLL_CONTAINER_HEIGHT)
12024 .with_flex_shrink(0.0),
12025 )
12026 .with_viewport_layout(
12027 LayoutStyle::column()
12028 .with_width(0.0)
12029 .with_flex_grow(1.0)
12030 .with_height(TIMELINE_VIEWPORT_HEIGHT)
12031 .with_flex_shrink(1.0),
12032 )
12033 .with_horizontal_scrollbar(
12034 scrollbar_widgets::ScrollbarOptions::default()
12035 .with_action("timeline.horizontal-scrollbar"),
12036 )
12037 .with_accessibility_label("Timeline horizontal scroller"),
12038 );
12039 let content = ui.add_child(
12040 nodes.viewport,
12041 UiNode::container(
12042 "timeline.content",
12043 LayoutStyle::column()
12044 .with_width(TIMELINE_CONTENT_WIDTH)
12045 .with_height(TIMELINE_VIEWPORT_HEIGHT)
12046 .with_flex_shrink(0.0),
12047 ),
12048 );
12049 let mut ruler_options = ext_widgets::TimelineRulerOptions::default();
12050 ruler_options.height = 40.0;
12051 ruler_options.layout = LayoutStyle::new()
12052 .with_width(TIMELINE_CONTENT_WIDTH)
12053 .with_height(40.0)
12054 .with_flex_shrink(0.0);
12055 ruler_options.accessibility_label = Some("Editing timeline ruler".to_string());
12056 ruler_options.accessibility_hint =
12057 Some("Shows seconds for the visible timeline clips".to_string());
12058 ext_widgets::timeline_ruler(
12059 ui,
12060 content,
12061 "timeline.ruler",
12062 ext_widgets::RulerSpec {
12063 range,
12064 width: TIMELINE_CONTENT_WIDTH,
12065 major_step: 4.0,
12066 minor_step: 1.0,
12067 label_every: 1,
12068 },
12069 ruler_options,
12070 );
12071 ui.add_child(
12072 content,
12073 UiNode::scene(
12074 "timeline.tracks",
12075 timeline_track_primitives(range, TIMELINE_CONTENT_WIDTH),
12076 LayoutStyle::new()
12077 .with_width(TIMELINE_CONTENT_WIDTH)
12078 .with_height(132.0)
12079 .with_flex_shrink(0.0),
12080 ),
12081 );
12082}
12083
12084fn timeline_track_primitives(range: ext_widgets::TimelineRange, width: f32) -> Vec<ScenePrimitive> {
12085 let mut primitives = Vec::new();
12086 let lane_height = 36.0;
12087 let lane_gap = 8.0;
12088 let lanes = [
12089 ("Video", 0.0, color(16, 22, 30)),
12090 ("Audio", lane_height + lane_gap, color(13, 20, 27)),
12091 ("Notes", (lane_height + lane_gap) * 2.0, color(16, 22, 30)),
12092 ];
12093
12094 for (label, y, fill) in lanes {
12095 primitives.push(ScenePrimitive::Rect(
12096 PaintRect::solid(UiRect::new(0.0, y, width, lane_height), fill)
12097 .stroke(AlignedStroke::inside(StrokeStyle::new(
12098 color(38, 49, 64),
12099 1.0,
12100 )))
12101 .corner_radii(CornerRadii::uniform(2.0)),
12102 ));
12103 primitives.push(ScenePrimitive::Text(
12104 PaintText::new(
12105 label,
12106 UiRect::new(8.0, y + 8.0, 72.0, 18.0),
12107 text(10.0, color(116, 128, 145)),
12108 )
12109 .multiline(false),
12110 ));
12111 }
12112
12113 for second in (0..=48).step_by(4) {
12114 let x = range.value_to_x(second as f64, width);
12115 primitives.push(ScenePrimitive::Line {
12116 from: UiPoint::new(x, 0.0),
12117 to: UiPoint::new(x, 124.0),
12118 stroke: StrokeStyle::new(color(34, 44, 58), 1.0),
12119 });
12120 }
12121
12122 push_timeline_clip(
12123 &mut primitives,
12124 range,
12125 width,
12126 "Intro",
12127 2.0,
12128 10.0,
12129 0.0,
12130 color(57, 126, 207),
12131 );
12132 push_timeline_clip(
12133 &mut primitives,
12134 range,
12135 width,
12136 "Cutaway",
12137 12.0,
12138 24.0,
12139 0.0,
12140 color(95, 107, 212),
12141 );
12142 push_timeline_clip(
12143 &mut primitives,
12144 range,
12145 width,
12146 "Final",
12147 28.0,
12148 44.0,
12149 0.0,
12150 color(68, 153, 122),
12151 );
12152 push_timeline_clip(
12153 &mut primitives,
12154 range,
12155 width,
12156 "Music bed",
12157 0.0,
12158 48.0,
12159 lane_height + lane_gap,
12160 color(205, 160, 71),
12161 );
12162 push_timeline_clip(
12163 &mut primitives,
12164 range,
12165 width,
12166 "Voiceover",
12167 8.0,
12168 18.0,
12169 lane_height + lane_gap,
12170 color(183, 107, 185),
12171 );
12172
12173 for (second, label) in [(6.0, "Beat"), (21.0, "Cut"), (37.0, "Cue")] {
12174 let x = range.value_to_x(second, width);
12175 let y = (lane_height + lane_gap) * 2.0 + 8.0;
12176 primitives.push(ScenePrimitive::Polygon {
12177 points: vec![
12178 UiPoint::new(x, y),
12179 UiPoint::new(x + 8.0, y + 8.0),
12180 UiPoint::new(x, y + 16.0),
12181 UiPoint::new(x - 8.0, y + 8.0),
12182 ],
12183 fill: color(245, 198, 83),
12184 stroke: Some(StrokeStyle::new(color(255, 234, 178), 1.0)),
12185 });
12186 primitives.push(ScenePrimitive::Text(
12187 PaintText::new(
12188 label,
12189 UiRect::new(x + 12.0, y - 1.0, 72.0, 18.0),
12190 text(10.0, color(225, 233, 244)),
12191 )
12192 .multiline(false),
12193 ));
12194 }
12195
12196 let playhead_x = range.value_to_x(18.5, width);
12197 primitives.push(ScenePrimitive::Line {
12198 from: UiPoint::new(playhead_x, 0.0),
12199 to: UiPoint::new(playhead_x, 124.0),
12200 stroke: StrokeStyle::new(ColorRgba::new(255, 120, 96, 255), 2.0),
12201 });
12202 primitives.push(ScenePrimitive::Text(
12203 PaintText::new(
12204 "Playhead 18.5s",
12205 UiRect::new(playhead_x + 8.0, 106.0, 120.0, 18.0),
12206 text(10.0, ColorRgba::new(255, 172, 154, 255)),
12207 )
12208 .multiline(false),
12209 ));
12210
12211 primitives
12212}
12213
12214#[allow(clippy::too_many_arguments)]
12215fn push_timeline_clip(
12216 primitives: &mut Vec<ScenePrimitive>,
12217 range: ext_widgets::TimelineRange,
12218 width: f32,
12219 label: &'static str,
12220 start: f64,
12221 end: f64,
12222 lane_y: f32,
12223 fill: ColorRgba,
12224) {
12225 let x = range.value_to_x(start, width);
12226 let right = range.value_to_x(end, width);
12227 let rect = UiRect::new(x, lane_y + 6.0, (right - x).max(1.0), 24.0);
12228 primitives.push(ScenePrimitive::Rect(
12229 PaintRect::solid(rect, fill)
12230 .stroke(AlignedStroke::inside(StrokeStyle::new(
12231 ColorRgba::new(230, 240, 255, 96),
12232 1.0,
12233 )))
12234 .corner_radii(CornerRadii::uniform(4.0)),
12235 ));
12236 primitives.push(ScenePrimitive::Text(
12237 PaintText::new(label, rect, text(10.0, color(245, 248, 252)))
12238 .horizontal_align(TextHorizontalAlign::Center)
12239 .vertical_align(TextVerticalAlign::Center)
12240 .multiline(false),
12241 ));
12242}
12243
12244fn theme_demo_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState, theme: &Theme) {
12245 let body = section(ui, parent, "theme", "Theme");
12246 widgets::label(
12247 ui,
12248 body,
12249 "theme.current",
12250 format!("Current theme: {}", theme.name),
12251 themed_text(theme, 14.0),
12252 LayoutStyle::new().with_width_percent(1.0),
12253 );
12254
12255 let choices = wrapping_row(ui, body, "theme.choices", 8.0);
12256 for choice in [
12257 ShowcaseThemeChoice::Light,
12258 ShowcaseThemeChoice::Dark,
12259 ShowcaseThemeChoice::Bubblegum,
12260 ] {
12261 theme_choice_button(
12262 ui,
12263 choices,
12264 choice,
12265 state.showcase_theme == choice,
12266 choice.theme(),
12267 );
12268 }
12269
12270 let swatches = wrapping_row(ui, body, "theme.swatches", 8.0);
12271 theme_swatch(
12272 ui,
12273 swatches,
12274 "theme.swatch.canvas",
12275 "Canvas",
12276 theme.colors.canvas,
12277 theme,
12278 );
12279 theme_swatch(
12280 ui,
12281 swatches,
12282 "theme.swatch.surface",
12283 "Surface",
12284 theme.colors.surface,
12285 theme,
12286 );
12287 theme_swatch(
12288 ui,
12289 swatches,
12290 "theme.swatch.accent",
12291 "Accent",
12292 theme.colors.accent,
12293 theme,
12294 );
12295 theme_swatch(
12296 ui,
12297 swatches,
12298 "theme.swatch.selected",
12299 "Selected",
12300 theme.colors.selected,
12301 theme,
12302 );
12303
12304 let preview = ui.add_child(
12305 body,
12306 UiNode::container(
12307 "theme.preview",
12308 LayoutStyle::column()
12309 .with_width_percent(1.0)
12310 .with_padding(12.0)
12311 .with_gap(10.0)
12312 .with_flex_shrink(0.0),
12313 )
12314 .with_visual(UiVisual::panel(
12315 theme.colors.surface,
12316 Some(theme.stroke.surface),
12317 theme.radius.md,
12318 ))
12319 .with_accessibility(
12320 AccessibilityMeta::new(AccessibilityRole::Group).label("Theme preview"),
12321 ),
12322 );
12323 widgets::label(
12324 ui,
12325 preview,
12326 "theme.preview.title",
12327 "Preview controls",
12328 themed_text(theme, 13.0),
12329 LayoutStyle::new().with_width_percent(1.0),
12330 );
12331 let preview_row = row(ui, preview, "theme.preview.controls", 8.0);
12332 let mut primary = themed_button_options(
12333 theme,
12334 "theme.preview.primary",
12335 ComponentState::ACTIVE,
12336 LayoutStyle::new().with_height(34.0),
12337 );
12338 primary.accessibility_label = Some("Primary preview button".to_owned());
12339 widgets::button(ui, preview_row, "theme.preview.primary", "Primary", primary);
12340 let mut secondary = themed_button_options(
12341 theme,
12342 "theme.preview.secondary",
12343 ComponentState::NORMAL,
12344 LayoutStyle::new().with_height(34.0),
12345 );
12346 secondary.accessibility_label = Some("Secondary preview button".to_owned());
12347 widgets::button(
12348 ui,
12349 preview_row,
12350 "theme.preview.secondary",
12351 "Secondary",
12352 secondary,
12353 );
12354 let mut help = themed_muted_text(theme, 12.0);
12355 help.wrap = TextWrap::WordOrGlyph;
12356 widgets::label(
12357 ui,
12358 preview,
12359 "theme.preview.copy",
12360 "The selected theme drives the app background, right panel, floating windows, and this preview.",
12361 help,
12362 LayoutStyle::new().with_width_percent(1.0),
12363 );
12364}
12365
12366fn theme_choice_button(
12367 ui: &mut UiDocument,
12368 parent: UiNodeId,
12369 choice: ShowcaseThemeChoice,
12370 selected: bool,
12371 preview_theme: Theme,
12372) {
12373 let mut options = themed_button_options(
12374 &preview_theme,
12375 choice.action(),
12376 if selected {
12377 ComponentState::SELECTED
12378 } else {
12379 ComponentState::NORMAL
12380 },
12381 LayoutStyle::new()
12382 .with_width(116.0)
12383 .with_height(34.0)
12384 .with_flex_shrink(0.0),
12385 )
12386 .with_action(choice.action());
12387 options.accessibility_label = Some(format!("Use {} theme", choice.label()));
12388 widgets::button(
12389 ui,
12390 parent,
12391 format!("theme.choice.{}", choice.label().to_ascii_lowercase()),
12392 choice.label(),
12393 options,
12394 );
12395}
12396
12397fn theme_swatch(
12398 ui: &mut UiDocument,
12399 parent: UiNodeId,
12400 name: &'static str,
12401 label: &'static str,
12402 swatch_color: ColorRgba,
12403 theme: &Theme,
12404) {
12405 let tile = ui.add_child(
12406 parent,
12407 UiNode::container(
12408 name,
12409 LayoutStyle::column()
12410 .with_width(92.0)
12411 .with_height(76.0)
12412 .with_padding(8.0)
12413 .with_gap(6.0)
12414 .with_flex_shrink(0.0),
12415 )
12416 .with_visual(UiVisual::panel(
12417 theme.colors.surface_muted,
12418 Some(theme.stroke.surface),
12419 4.0,
12420 ))
12421 .with_accessibility(AccessibilityMeta::new(AccessibilityRole::Group).label(label)),
12422 );
12423 ui.add_child(
12424 tile,
12425 UiNode::container(
12426 format!("{name}.color"),
12427 LayoutStyle::new()
12428 .with_width_percent(1.0)
12429 .with_height(26.0)
12430 .with_flex_shrink(0.0),
12431 )
12432 .with_visual(UiVisual::panel(
12433 swatch_color,
12434 Some(StrokeStyle::new(theme.colors.border_strong, 1.0)),
12435 4.0,
12436 )),
12437 );
12438 widgets::label(
12439 ui,
12440 tile,
12441 format!("{name}.label"),
12442 label,
12443 themed_muted_text(theme, 11.0),
12444 LayoutStyle::new().with_width_percent(1.0),
12445 );
12446}
12447
12448fn themed_button_options(
12449 theme: &Theme,
12450 action: impl Into<String>,
12451 state: ComponentState,
12452 layout: LayoutStyle,
12453) -> widgets::ButtonOptions {
12454 let mut options = widgets::ButtonOptions::new(layout).with_action(action.into());
12455 options.visual = theme.resolve_visual(ComponentRole::Button, state);
12456 options.hovered_visual =
12457 Some(theme.resolve_visual(ComponentRole::Button, ComponentState::HOVERED));
12458 options.pressed_visual =
12459 Some(theme.resolve_visual(ComponentRole::Button, ComponentState::PRESSED));
12460 options.pressed_hovered_visual =
12461 Some(theme.resolve_visual(ComponentRole::Button, ComponentState::PRESSED));
12462 options.focused_visual =
12463 Some(theme.resolve_visual(ComponentRole::Button, ComponentState::FOCUSED));
12464 options.disabled_visual =
12465 Some(theme.resolve_visual(ComponentRole::Button, ComponentState::DISABLED));
12466 options.text_style = theme.resolve_text(ComponentRole::Button, state);
12467 options
12468}
12469
12470fn styling_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12471 let preview_scene_size = style_preview_scene_size(state.styling);
12472 let preview_min_width = preview_scene_size.width + 16.0;
12473 let preview_min_height = preview_scene_size.height + 16.0;
12474 let body_min_width = STYLING_CONTROLS_WIDTH + 1.0 + preview_min_width + 20.0;
12475 let body = section_with_min_viewport(
12476 ui,
12477 parent,
12478 "styling",
12479 "Styling",
12480 UiSize::new(body_min_width, preview_min_height),
12481 );
12482 let grid_layout = operad::layout::with_grid_template_columns(
12483 Layout::grid()
12484 .size(LayoutSize::percent(1.0, 1.0))
12485 .gap(LayoutGap::points(10.0, 10.0))
12486 .to_layout_style(),
12487 [
12488 LayoutGridTrack::points(STYLING_CONTROLS_WIDTH),
12489 LayoutGridTrack::points(1.0),
12490 LayoutGridTrack::minmax_points_fraction(preview_min_width, 1.0),
12491 ],
12492 );
12493 let grid = ui.add_child(body, UiNode::container("styling.grid", grid_layout));
12494 let controls = ui.add_child(
12495 grid,
12496 UiNode::container(
12497 "styling.controls",
12498 LayoutStyle::column()
12499 .with_width(STYLING_CONTROLS_WIDTH)
12500 .with_height_percent(1.0)
12501 .with_flex_shrink(0.0)
12502 .gap(6.0),
12503 ),
12504 );
12505 style_edge_group(
12506 ui,
12507 controls,
12508 "styling.inner",
12509 "Inner margin",
12510 "styling.inner_same",
12511 state.styling.inner_same,
12512 [
12513 ("Left", "styling.inner", state.styling.inner_margin),
12514 ("Right", "styling.inner_right", state.styling.inner_right),
12515 ("Top", "styling.inner_top", state.styling.inner_top),
12516 ("Bottom", "styling.inner_bottom", state.styling.inner_bottom),
12517 ],
12518 0.0..32.0,
12519 );
12520 style_edge_group(
12521 ui,
12522 controls,
12523 "styling.outer",
12524 "Outer margin",
12525 "styling.outer_same",
12526 state.styling.outer_same,
12527 [
12528 ("Left", "styling.outer", state.styling.outer_margin),
12529 ("Right", "styling.outer_right", state.styling.outer_right),
12530 ("Top", "styling.outer_top", state.styling.outer_top),
12531 ("Bottom", "styling.outer_bottom", state.styling.outer_bottom),
12532 ],
12533 0.0..40.0,
12534 );
12535 style_edge_group(
12536 ui,
12537 controls,
12538 "styling.radius",
12539 "Corner radius",
12540 "styling.radius_same",
12541 state.styling.radius_same,
12542 [
12543 ("NW", "styling.radius", state.styling.corner_radius),
12544 ("NE", "styling.radius_ne", state.styling.corner_ne),
12545 ("SW", "styling.radius_sw", state.styling.corner_sw),
12546 ("SE", "styling.radius_se", state.styling.corner_se),
12547 ],
12548 0.0..28.0,
12549 );
12550 style_fill_group(ui, controls, state);
12551 style_stroke_group(ui, controls, state);
12552 style_shadow_group(ui, controls, state);
12553 widgets::separator(
12554 ui,
12555 grid,
12556 "styling.preview.separator",
12557 widgets::SeparatorOptions::vertical().with_layout(
12558 LayoutStyle::new()
12559 .with_width(1.0)
12560 .with_height_percent(1.0)
12561 .with_flex_shrink(0.0),
12562 ),
12563 );
12564
12565 let preview = ui.add_child(
12566 grid,
12567 UiNode::container(
12568 "styling.preview",
12569 operad::layout::with_min_size(
12570 LayoutStyle::column()
12571 .with_width_percent(1.0)
12572 .with_height_percent(1.0)
12573 .with_flex_shrink(0.0)
12574 .padding(8.0),
12575 operad::layout::px(preview_min_width),
12576 operad::layout::px(preview_min_height),
12577 ),
12578 )
12579 .with_visual(UiVisual::panel(color(17, 20, 25), None, 0.0)),
12580 );
12581 style_preview(ui, preview, state.styling);
12582}
12583
12584#[allow(clippy::too_many_arguments)]
12585fn style_edge_group(
12586 ui: &mut UiDocument,
12587 parent: UiNodeId,
12588 name: &'static str,
12589 title: &'static str,
12590 same_action: &'static str,
12591 same: bool,
12592 values: [(&'static str, &'static str, f32); 4],
12593 range: std::ops::Range<f32>,
12594) {
12595 let group = style_control_group(ui, parent, format!("{name}.group"));
12596 style_group_title(ui, group, format!("{name}.title"), title);
12597 let fields = ui.add_child(
12598 group,
12599 UiNode::container(
12600 format!("{name}.fields"),
12601 LayoutStyle::column()
12602 .with_width(138.0)
12603 .with_flex_shrink(0.0)
12604 .gap(3.0),
12605 ),
12606 );
12607 style_compact_checkbox(ui, fields, same_action, "same", same);
12608 if same {
12609 style_number_row(ui, fields, values[0].1, "All", values[0].2, range, 0);
12610 } else {
12611 for (label, action, value) in values {
12612 style_number_row(ui, fields, action, label, value, range.clone(), 0);
12613 }
12614 }
12615}
12616
12617fn style_fill_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12618 let group = style_control_group(ui, parent, "styling.fill.group");
12619 style_group_title(ui, group, "styling.fill.title", "Fill");
12620 let fields = style_group_fields(
12621 ui,
12622 group,
12623 "styling.fill.fields",
12624 STYLING_WIDE_FIELDS_WIDTH,
12625 4.0,
12626 );
12627 style_color_button_row(
12628 ui,
12629 fields,
12630 "styling.fill_color_button",
12631 "",
12632 state.styling.fill_color(),
12633 "Pick fill color",
12634 );
12635 if state.styling_fill_picker_open {
12636 ext_widgets::color_picker(
12637 ui,
12638 fields,
12639 "styling.fill_picker",
12640 &state.styling_fill_picker,
12641 ext_widgets::ColorPickerOptions::default()
12642 .with_label("Fill")
12643 .with_action_prefix("styling.fill_picker"),
12644 );
12645 }
12646}
12647
12648fn style_stroke_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12649 let group = style_control_group(ui, parent, "styling.stroke.group");
12650 style_group_title(ui, group, "styling.stroke.title", "Stroke");
12651 let fields = style_group_fields(
12652 ui,
12653 group,
12654 "styling.stroke.fields",
12655 STYLING_WIDE_FIELDS_WIDTH,
12656 4.0,
12657 );
12658 let width_row = row(ui, fields, "styling.stroke.row", 6.0);
12659 style_inline_number(
12660 ui,
12661 width_row,
12662 "styling.stroke",
12663 "width",
12664 state.styling.stroke_width,
12665 0.0..STYLING_STROKE_MAX,
12666 1,
12667 );
12668 let mut options = widgets::SliderOptions::default()
12669 .with_layout(
12670 LayoutStyle::new()
12671 .with_width(60.0)
12672 .with_height(20.0)
12673 .with_flex_shrink(0.0),
12674 )
12675 .with_value_edit_action("styling.stroke");
12676 options.fill_color = color(120, 170, 230);
12677 widgets::slider(
12678 ui,
12679 width_row,
12680 "styling.stroke.slider",
12681 (state.styling.stroke_width / STYLING_STROKE_MAX).clamp(0.0, 1.0),
12682 0.0..1.0,
12683 options,
12684 );
12685 style_color_button_row(
12686 ui,
12687 fields,
12688 "styling.stroke_color_button",
12689 "",
12690 state.styling.stroke_color(),
12691 "Pick stroke color",
12692 );
12693 if state.styling_stroke_picker_open {
12694 ext_widgets::color_picker(
12695 ui,
12696 fields,
12697 "styling.stroke_picker",
12698 &state.styling_stroke_picker,
12699 ext_widgets::ColorPickerOptions::default()
12700 .with_label("Stroke color")
12701 .with_action_prefix("styling.stroke_picker"),
12702 );
12703 }
12704}
12705
12706fn style_shadow_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12707 let group = style_control_group(ui, parent, "styling.shadow.group");
12708 style_group_title(ui, group, "styling.shadow.title", "Shadow");
12709 let fields = style_group_fields(
12710 ui,
12711 group,
12712 "styling.shadow.fields",
12713 STYLING_WIDE_FIELDS_WIDTH,
12714 4.0,
12715 );
12716 let offsets = row(ui, fields, "styling.shadow.offsets", 6.0);
12717 style_inline_number(
12718 ui,
12719 offsets,
12720 "styling.shadow_x",
12721 "x",
12722 state.styling.shadow_x,
12723 -24.0..24.0,
12724 0,
12725 );
12726 style_inline_number(
12727 ui,
12728 offsets,
12729 "styling.shadow_y",
12730 "y",
12731 state.styling.shadow_y,
12732 -24.0..24.0,
12733 0,
12734 );
12735 let spread = row(ui, fields, "styling.shadow.blur_spread", 6.0);
12736 style_inline_number(
12737 ui,
12738 spread,
12739 "styling.shadow",
12740 "blur",
12741 state.styling.shadow_blur,
12742 0.0..32.0,
12743 0,
12744 );
12745 style_inline_number(
12746 ui,
12747 spread,
12748 "styling.shadow_spread",
12749 "spread",
12750 state.styling.shadow_spread,
12751 0.0..16.0,
12752 0,
12753 );
12754 style_color_button_row(
12755 ui,
12756 fields,
12757 "styling.shadow_color_button",
12758 "",
12759 state.styling.shadow_color(),
12760 "Pick shadow color",
12761 );
12762 if state.styling_shadow_picker_open {
12763 ext_widgets::color_picker(
12764 ui,
12765 fields,
12766 "styling.shadow_picker",
12767 &state.styling_shadow_picker,
12768 ext_widgets::ColorPickerOptions::default()
12769 .with_label("Shadow color")
12770 .with_action_prefix("styling.shadow_picker"),
12771 );
12772 }
12773}
12774
12775fn style_control_group(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>) -> UiNodeId {
12776 ui.add_child(
12777 parent,
12778 UiNode::container(
12779 name,
12780 LayoutStyle::row()
12781 .with_width_percent(1.0)
12782 .with_flex_shrink(0.0)
12783 .padding(4.0)
12784 .gap(8.0),
12785 )
12786 .with_visual(UiVisual::panel(color(23, 27, 33), None, 2.0)),
12787 )
12788}
12789
12790fn style_group_fields(
12791 ui: &mut UiDocument,
12792 parent: UiNodeId,
12793 name: impl Into<String>,
12794 width: f32,
12795 gap: f32,
12796) -> UiNodeId {
12797 ui.add_child(
12798 parent,
12799 UiNode::container(
12800 name,
12801 LayoutStyle::column()
12802 .with_width(width)
12803 .with_flex_shrink(0.0)
12804 .gap(gap),
12805 ),
12806 )
12807}
12808
12809fn style_group_title(
12810 ui: &mut UiDocument,
12811 parent: UiNodeId,
12812 name: impl Into<String>,
12813 label: &'static str,
12814) {
12815 widgets::label(
12816 ui,
12817 parent,
12818 name,
12819 label,
12820 text(12.0, color(166, 176, 190)),
12821 LayoutStyle::new()
12822 .with_width(88.0)
12823 .with_flex_shrink(0.0)
12824 .with_height(22.0),
12825 );
12826}
12827
12828fn style_color_button_row(
12829 ui: &mut UiDocument,
12830 parent: UiNodeId,
12831 action: &'static str,
12832 label: &'static str,
12833 value: ColorRgba,
12834 accessibility_label: &'static str,
12835) {
12836 let row = row(ui, parent, format!("{action}.row"), 8.0);
12837 if !label.is_empty() {
12838 widgets::label(
12839 ui,
12840 row,
12841 format!("{action}.label"),
12842 label,
12843 text(12.0, color(166, 176, 190)),
12844 LayoutStyle::new()
12845 .with_width(86.0)
12846 .with_flex_shrink(0.0)
12847 .with_height(24.0),
12848 );
12849 }
12850 ext_widgets::color_edit_button(
12851 ui,
12852 row,
12853 action,
12854 value,
12855 color_mini_button_options(action)
12856 .with_format(ext_widgets::ColorValueFormat::Rgba)
12857 .accessibility_label(accessibility_label),
12858 );
12859 widgets::label(
12860 ui,
12861 row,
12862 format!("{action}.value"),
12863 ext_widgets::color_picker::format_hex_color(value, value.a < 255),
12864 text(12.0, color(226, 232, 242)),
12865 LayoutStyle::new().with_width(96.0).with_height(24.0),
12866 );
12867}
12868
12869fn style_number_row(
12870 ui: &mut UiDocument,
12871 parent: UiNodeId,
12872 name: &'static str,
12873 label: &'static str,
12874 value: f32,
12875 range: std::ops::Range<f32>,
12876 decimals: u8,
12877) {
12878 let row = row(ui, parent, format!("{name}.row"), 6.0);
12879 widgets::label(
12880 ui,
12881 row,
12882 format!("{name}.label"),
12883 label,
12884 text(12.0, color(166, 176, 190)),
12885 LayoutStyle::new().with_width(48.0).with_height(22.0),
12886 );
12887 style_value_input(ui, row, name, value, range, decimals);
12888}
12889
12890fn style_inline_number(
12891 ui: &mut UiDocument,
12892 parent: UiNodeId,
12893 name: &'static str,
12894 label: &'static str,
12895 value: f32,
12896 range: std::ops::Range<f32>,
12897 decimals: u8,
12898) {
12899 let row = compact_row(ui, parent, format!("{name}.inline"), 3.0);
12900 widgets::label(
12901 ui,
12902 row,
12903 format!("{name}.inline_label"),
12904 format!("{label}:"),
12905 text(12.0, color(166, 176, 190)),
12906 LayoutStyle::new()
12907 .with_width(if label.len() > 1 { 42.0 } else { 16.0 })
12908 .with_height(22.0),
12909 );
12910 style_value_input(ui, row, name, value, range, decimals);
12911}
12912
12913fn style_value_input(
12914 ui: &mut UiDocument,
12915 parent: UiNodeId,
12916 name: &'static str,
12917 value: f32,
12918 range: std::ops::Range<f32>,
12919 decimals: u8,
12920) {
12921 let mut options = widgets::DragValueOptions::default()
12922 .with_layout(
12923 LayoutStyle::row()
12924 .with_width(STYLING_VALUE_INPUT_WIDTH)
12925 .with_height(22.0)
12926 .with_flex_shrink(0.0)
12927 .with_align_items(taffy::prelude::AlignItems::Center)
12928 .with_justify_content(taffy::prelude::JustifyContent::Center)
12929 .with_padding(4.0),
12930 )
12931 .with_range(ext_widgets::NumericRange::new(
12932 f64::from(range.start),
12933 f64::from(range.end),
12934 ))
12935 .with_precision(ext_widgets::NumericPrecision::decimals(decimals))
12936 .with_action(name);
12937 options.text_style = text(12.0, color(226, 232, 242));
12938 widgets::drag_value_input(ui, parent, name, f64::from(value), options);
12939}
12940
12941fn style_compact_checkbox(
12942 ui: &mut UiDocument,
12943 parent: UiNodeId,
12944 name: &'static str,
12945 label: &'static str,
12946 checked: bool,
12947) {
12948 let mut options = widgets::CheckboxOptions::default().with_action(name);
12949 options.layout = LayoutStyle::new().with_width(92.0).with_height(22.0);
12950 options.text_style = text(12.0, color(220, 228, 238));
12951 widgets::checkbox(ui, parent, name, label, checked, options);
12952}
12953
12954fn compact_row(
12955 ui: &mut UiDocument,
12956 parent: UiNodeId,
12957 name: impl Into<String>,
12958 gap: f32,
12959) -> UiNodeId {
12960 ui.add_child(
12961 parent,
12962 UiNode::container(
12963 name,
12964 LayoutStyle::row()
12965 .with_height(22.0)
12966 .with_flex_shrink(0.0)
12967 .with_align_items(taffy::prelude::AlignItems::Center)
12968 .gap(gap),
12969 ),
12970 )
12971}
12972
12973fn color_mini_button_options(action: &'static str) -> ext_widgets::ColorButtonOptions {
12974 ext_widgets::ColorButtonOptions::default()
12975 .with_layout(LayoutStyle::size(28.0, 24.0).with_flex_shrink(0.0))
12976 .with_swatch_size(UiSize::new(22.0, 18.0))
12977 .with_action(action)
12978 .show_label(false)
12979}
12980
12981fn style_preview(ui: &mut UiDocument, parent: UiNodeId, styling: StylingState) {
12982 let (frame, text_rect) = style_preview_rects(styling);
12983 let scene_size = style_preview_scene_size(styling);
12984 ui.add_child(
12985 parent,
12986 UiNode::scene(
12987 "styling.preview.scene",
12988 vec![
12989 ScenePrimitive::Rect(
12990 PaintRect::solid(frame, styling.fill_color())
12991 .stroke(AlignedStroke::inside(StrokeStyle::new(
12992 styling.stroke_color(),
12993 styling.stroke_width,
12994 )))
12995 .corner_radii(styling.radii())
12996 .effect(PaintEffect::shadow(
12997 styling.shadow_color(),
12998 UiPoint::new(styling.shadow_x, styling.shadow_y),
12999 styling.shadow_blur,
13000 styling.shadow_spread,
13001 )),
13002 ),
13003 ScenePrimitive::Text(
13004 PaintText::new("Content", text_rect, text(13.0, color(255, 255, 255)))
13005 .horizontal_align(TextHorizontalAlign::Center)
13006 .vertical_align(TextVerticalAlign::Center)
13007 .multiline(false),
13008 ),
13009 ],
13010 operad::layout::with_min_size(
13011 LayoutStyle::new()
13012 .with_width_percent(1.0)
13013 .with_height(180.0)
13014 .with_flex_shrink(0.0),
13015 operad::layout::px(scene_size.width),
13016 operad::layout::px(scene_size.height),
13017 ),
13018 ),
13019 );
13020}
13021
13022fn style_preview_rects(styling: StylingState) -> (UiRect, UiRect) {
13023 let outer = styling.outer_edges();
13024 let inner = styling.inner_edges();
13025 let frame = UiRect::new(
13026 22.0 + outer[0],
13027 28.0 + outer[2],
13028 108.0 + inner[0] + inner[1],
13029 40.0 + inner[2] + inner[3],
13030 );
13031 let text_rect = UiRect::new(
13032 frame.x + inner[0],
13033 frame.y + inner[2],
13034 (frame.width - inner[0] - inner[1]).max(1.0),
13035 (frame.height - inner[2] - inner[3]).max(1.0),
13036 );
13037 (frame, text_rect)
13038}
13039
13040fn style_preview_scene_size(styling: StylingState) -> UiSize {
13041 let (frame, text_rect) = style_preview_rects(styling);
13042 let shadow_outset = styling.shadow_blur.max(0.0) + styling.shadow_spread.max(0.0);
13043 let shadow_bounds = UiRect::new(
13044 frame.x + styling.shadow_x - shadow_outset,
13045 frame.y + styling.shadow_y - shadow_outset,
13046 frame.width + shadow_outset * 2.0,
13047 frame.height + shadow_outset * 2.0,
13048 );
13049 let right = frame
13050 .right()
13051 .max(text_rect.right())
13052 .max(shadow_bounds.right());
13053 let bottom = frame
13054 .bottom()
13055 .max(text_rect.bottom())
13056 .max(shadow_bounds.bottom())
13057 .max(180.0);
13058 UiSize::new(right.ceil().max(1.0), bottom.ceil().max(1.0))
13059}
13060
13061fn slider_options(state: &ShowcaseState, width: f32) -> widgets::SliderOptions {
13062 let mut options = widgets::SliderOptions::default().with_layout(
13063 LayoutStyle::new()
13064 .with_width(width)
13065 .with_height(24.0)
13066 .with_flex_shrink(0.0),
13067 );
13068 options.fill_color = if state.slider_trailing_color {
13069 state.slider_trailing_picker.value()
13070 } else {
13071 color(42, 49, 58)
13072 };
13073 options.thumb_shape = match state.slider_thumb_shape {
13074 SliderThumbChoice::Circle => widgets::slider::SliderThumbShape::Circle,
13075 SliderThumbChoice::Square => widgets::slider::SliderThumbShape::Square,
13076 SliderThumbChoice::Rectangle => widgets::slider::SliderThumbShape::Rectangle,
13077 };
13078 options.thumb_visual = UiVisual::panel(
13079 state.slider_thumb_picker.value(),
13080 Some(StrokeStyle::new(color(79, 93, 113), 1.0)),
13081 6.0,
13082 );
13083 options
13084}
13085
13086#[allow(clippy::field_reassign_with_default)]
13087fn slider_number_input(
13088 ui: &mut UiDocument,
13089 parent: UiNodeId,
13090 name: &'static str,
13091 input: &TextInputState,
13092 focused: FocusedTextInput,
13093 state: &ShowcaseState,
13094 width: f32,
13095) {
13096 let mut options = TextInputOptions::default();
13097 options.layout = LayoutStyle::new().with_width(width).with_height(28.0);
13098 options.text_style = text(12.0, color(230, 236, 246));
13099 options.placeholder_style = text(12.0, color(144, 156, 174));
13100 options.edit_action = Some(format!("{name}.edit").into());
13101 options.focused = state.focused_text == Some(focused);
13102 options.caret_visible = caret_visible(state.caret_phase);
13103 widgets::text_input(ui, parent, name, input, options);
13104}
13105
13106fn form_status_chip(
13107 ui: &mut UiDocument,
13108 parent: UiNodeId,
13109 name: &'static str,
13110 label: &'static str,
13111 active: bool,
13112) {
13113 let chip = ui.add_child(
13114 parent,
13115 UiNode::container(
13116 name,
13117 LayoutStyle::new()
13118 .with_width(82.0)
13119 .with_height(24.0)
13120 .with_padding(4.0)
13121 .with_flex_shrink(0.0),
13122 )
13123 .with_visual(UiVisual::panel(
13124 if active {
13125 color(35, 74, 54)
13126 } else {
13127 color(28, 34, 43)
13128 },
13129 Some(StrokeStyle::new(
13130 if active {
13131 color(90, 160, 112)
13132 } else {
13133 color(60, 72, 88)
13134 },
13135 1.0,
13136 )),
13137 4.0,
13138 )),
13139 );
13140 widgets::label(
13141 ui,
13142 chip,
13143 format!("{name}.label"),
13144 label,
13145 text(11.0, color(218, 228, 240)),
13146 LayoutStyle::new()
13147 .with_width_percent(1.0)
13148 .with_height_percent(1.0),
13149 );
13150}
13151
13152fn profile_form_summary(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
13153 let has_errors = widgets::form_has_errors(&state.form);
13154 let title = profile_form_summary_title(state, has_errors);
13155 let detail = format!(
13156 "{} | {} | {}",
13157 profile_summary_value(state.form_name_text.text(), "No name"),
13158 profile_summary_value(state.form_email_text.text(), "No email"),
13159 profile_summary_value(state.form_role_text.text(), "No role"),
13160 );
13161 let hint = profile_form_summary_hint(state, has_errors);
13162 let stroke = if has_errors {
13163 color(196, 94, 104)
13164 } else if state.form.dirty {
13165 color(205, 160, 71)
13166 } else if state.form.submitted {
13167 color(91, 164, 119)
13168 } else {
13169 color(60, 72, 88)
13170 };
13171 let summary = ui.add_child(
13172 parent,
13173 UiNode::container(
13174 "forms.profile.summary",
13175 LayoutStyle::column()
13176 .with_width_percent(1.0)
13177 .with_padding(10.0)
13178 .with_gap(4.0)
13179 .with_flex_shrink(0.0),
13180 )
13181 .with_visual(UiVisual::panel(
13182 color(20, 25, 32),
13183 Some(StrokeStyle::new(stroke, 1.0)),
13184 4.0,
13185 ))
13186 .with_accessibility(
13187 AccessibilityMeta::new(AccessibilityRole::Group)
13188 .label("Live profile summary")
13189 .value(format!("{title}. {detail}. {hint}")),
13190 ),
13191 );
13192 widgets::label(
13193 ui,
13194 summary,
13195 "forms.profile.summary.title",
13196 title,
13197 text(13.0, color(232, 240, 250)),
13198 LayoutStyle::new().with_width_percent(1.0),
13199 );
13200 widgets::label(
13201 ui,
13202 summary,
13203 "forms.profile.summary.detail",
13204 detail,
13205 text(12.0, color(186, 198, 216)),
13206 LayoutStyle::new().with_width_percent(1.0),
13207 );
13208 widgets::label(
13209 ui,
13210 summary,
13211 "forms.profile.summary.hint",
13212 hint,
13213 text(11.0, color(154, 166, 184)),
13214 LayoutStyle::new().with_width_percent(1.0),
13215 );
13216}
13217
13218fn profile_form_summary_title(state: &ShowcaseState, has_errors: bool) -> &'static str {
13219 if has_errors {
13220 "Profile needs fixes"
13221 } else if state.form.submitted {
13222 "Profile submitted"
13223 } else if state.form.dirty {
13224 "Profile draft"
13225 } else {
13226 "Profile saved"
13227 }
13228}
13229
13230fn profile_form_summary_hint(state: &ShowcaseState, has_errors: bool) -> &'static str {
13231 if has_errors {
13232 "Fix validation errors before applying or submitting."
13233 } else if state.form.dirty {
13234 "Apply saves the draft; Submit saves and marks it submitted."
13235 } else if state.form.submitted {
13236 "Submission completed. Apply stays disabled until something changes."
13237 } else {
13238 "No pending changes. Submit marks the saved profile submitted."
13239 }
13240}
13241
13242fn profile_summary_value<'a>(value: &'a str, empty: &'static str) -> &'a str {
13243 let value = value.trim();
13244 if value.is_empty() {
13245 empty
13246 } else {
13247 value
13248 }
13249}
13250
13251#[allow(clippy::field_reassign_with_default)]
13252fn form_text_field(
13253 ui: &mut UiDocument,
13254 parent: UiNodeId,
13255 name: &'static str,
13256 input: &TextInputState,
13257 focused: FocusedTextInput,
13258 state: &ShowcaseState,
13259) {
13260 let mut options = TextInputOptions::default();
13261 options.layout = LayoutStyle::new().with_width_percent(1.0).with_height(30.0);
13262 options.text_style = text(12.0, color(230, 236, 246));
13263 options.placeholder_style = text(12.0, color(144, 156, 174));
13264 options.placeholder = "Required".to_string();
13265 options.edit_action = Some(format!("{name}.edit").into());
13266 options.focused = state.focused_text == Some(focused);
13267 options.caret_visible = caret_visible(state.caret_phase);
13268 widgets::text_input(ui, parent, name, input, options);
13269}
13270
13271fn profile_email_valid(email: &str) -> bool {
13272 let email = email.trim();
13273 let Some((local, domain)) = email.split_once('@') else {
13274 return false;
13275 };
13276 !local.is_empty() && domain.contains('.') && !domain.ends_with('.')
13277}
13278
13279fn drag_source_layout() -> LayoutStyle {
13280 LayoutStyle::row()
13281 .with_width(128.0)
13282 .with_height(40.0)
13283 .with_padding(8.0)
13284 .with_gap(6.0)
13285 .with_flex_shrink(0.0)
13286}
13287
13288fn drop_zone_layout() -> LayoutStyle {
13289 LayoutStyle::column()
13290 .with_width(128.0)
13291 .with_height(78.0)
13292 .with_padding(10.0)
13293 .with_gap(6.0)
13294 .with_flex_shrink(0.0)
13295}
13296
13297fn dnd_operation_chip(
13298 ui: &mut UiDocument,
13299 parent: UiNodeId,
13300 name: &'static str,
13301 label: &'static str,
13302) {
13303 let chip = ui.add_child(
13304 parent,
13305 UiNode::container(
13306 name,
13307 LayoutStyle::new()
13308 .with_width(58.0)
13309 .with_height(22.0)
13310 .with_padding(3.0)
13311 .with_flex_shrink(0.0),
13312 )
13313 .with_visual(UiVisual::panel(
13314 color(26, 32, 42),
13315 Some(StrokeStyle::new(color(62, 76, 94), 1.0)),
13316 3.0,
13317 )),
13318 );
13319 widgets::label(
13320 ui,
13321 chip,
13322 format!("{name}.label"),
13323 label,
13324 text(11.0, color(190, 204, 222)),
13325 LayoutStyle::new()
13326 .with_width_percent(1.0)
13327 .with_height_percent(1.0),
13328 );
13329}
13330
13331fn media_preview_image_layout() -> LayoutStyle {
13332 LayoutStyle::size(46.0, 46.0).with_flex_shrink(0.0)
13333}
13334
13335fn media_icon_columns(state: &ShowcaseState) -> usize {
13336 let theme = state.app_theme();
13337 let options = showcase_desktop_options(state.last_desktop_size, &theme);
13338 let window_width = state
13339 .desktop
13340 .size("media", default_window_size("media"))
13341 .width;
13342 let content_width = (window_width - options.content_padding * 2.0).max(MEDIA_ICON_TILE_WIDTH);
13343 let pitch = MEDIA_ICON_TILE_WIDTH + MEDIA_ICON_GRID_GAP;
13344 (((content_width + MEDIA_ICON_GRID_GAP) / pitch).floor() as usize).clamp(1, MEDIA_ICON_COLUMNS)
13345}
13346
13347fn media_icon_grid_width(columns: usize) -> f32 {
13348 let columns = columns.max(1);
13349 columns as f32 * MEDIA_ICON_TILE_WIDTH + columns.saturating_sub(1) as f32 * MEDIA_ICON_GRID_GAP
13350}
13351
13352fn media_icon_grid_height(columns: usize, item_count: usize) -> f32 {
13353 let columns = columns.max(1);
13354 let rows = item_count.div_ceil(columns).max(1);
13355 rows as f32 * MEDIA_ICON_TILE_HEIGHT + rows.saturating_sub(1) as f32 * MEDIA_ICON_GRID_GAP
13356}
13357
13358fn media_icon_grid(
13359 ui: &mut UiDocument,
13360 parent: UiNodeId,
13361 name: impl Into<String>,
13362 columns: usize,
13363 item_count: usize,
13364) -> UiNodeId {
13365 let columns = columns.clamp(1, MEDIA_ICON_COLUMNS);
13366 let rows = item_count.div_ceil(columns).max(1);
13367 let width = media_icon_grid_width(columns);
13368 let height = media_icon_grid_height(columns, item_count);
13369 let layout = operad::layout::with_grid_template_rows(
13370 operad::layout::with_grid_template_columns(
13371 Layout::grid()
13372 .size(LayoutSize::points(width, height))
13373 .gap(LayoutGap::points(MEDIA_ICON_GRID_GAP, MEDIA_ICON_GRID_GAP))
13374 .flex(0.0, 0.0, LayoutDimension::Auto)
13375 .to_layout_style(),
13376 (0..columns).map(|_| LayoutGridTrack::points(MEDIA_ICON_TILE_WIDTH)),
13377 ),
13378 (0..rows).map(|_| LayoutGridTrack::points(MEDIA_ICON_TILE_HEIGHT)),
13379 );
13380 ui.add_child(parent, UiNode::container(name, layout))
13381}
13382
13383fn media_icon_tile(ui: &mut UiDocument, parent: UiNodeId, icon: BuiltInIcon) {
13384 let name = icon.key().replace('.', "_").replace('-', "_");
13385 let tile = ui.add_child(
13386 parent,
13387 UiNode::container(
13388 format!("media.icon_tile.{name}"),
13389 LayoutStyle::column()
13390 .with_width(MEDIA_ICON_TILE_WIDTH)
13391 .with_height(MEDIA_ICON_TILE_HEIGHT)
13392 .with_padding(6.0)
13393 .with_gap(4.0)
13394 .with_flex_shrink(0.0),
13395 )
13396 .with_visual(UiVisual::panel(
13397 color(17, 22, 30),
13398 Some(StrokeStyle::new(color(50, 62, 78), 1.0)),
13399 4.0,
13400 )),
13401 );
13402 widgets::image(
13403 ui,
13404 tile,
13405 format!("media.icon.{name}"),
13406 icon_image(icon),
13407 widgets::ImageOptions::default()
13408 .with_layout(LayoutStyle::size(28.0, 28.0))
13409 .with_accessibility_label(icon.label()),
13410 );
13411 widgets::label(
13412 ui,
13413 tile,
13414 format!("media.icon_label.{name}"),
13415 icon.label(),
13416 text(9.0, color(180, 194, 214)),
13417 LayoutStyle::new().with_width_percent(1.0).with_height(30.0),
13418 );
13419}
13420
13421fn slider_checkbox(
13422 ui: &mut UiDocument,
13423 parent: UiNodeId,
13424 name: &'static str,
13425 label: &'static str,
13426 checked: bool,
13427) {
13428 slider_checkbox_with_layout(
13429 ui,
13430 parent,
13431 name,
13432 label,
13433 checked,
13434 LayoutStyle::new().with_width_percent(1.0).with_height(30.0),
13435 );
13436}
13437
13438fn slider_checkbox_with_layout(
13439 ui: &mut UiDocument,
13440 parent: UiNodeId,
13441 name: &'static str,
13442 label: &'static str,
13443 checked: bool,
13444 layout: LayoutStyle,
13445) {
13446 let mut options = widgets::CheckboxOptions::default().with_action(name);
13447 options.layout = layout;
13448 options.text_style = text(12.0, color(220, 228, 238));
13449 widgets::checkbox(ui, parent, name, label, checked, options);
13450}
13451
13452fn choice_button(
13453 ui: &mut UiDocument,
13454 parent: UiNodeId,
13455 name: &'static str,
13456 label: &'static str,
13457 selected: bool,
13458) {
13459 let mut options =
13460 widgets::ButtonOptions::new(LayoutStyle::new().with_width(78.0).with_height(28.0))
13461 .with_action(name);
13462 options.visual = if selected {
13463 button_visual(48, 112, 184)
13464 } else {
13465 button_visual(38, 46, 58)
13466 };
13467 options.hovered_visual = Some(button_visual(65, 86, 106));
13468 options.pressed_visual = Some(button_visual(34, 54, 84));
13469 options.text_style = text(12.0, color(238, 244, 252));
13470 widgets::button(ui, parent, name, label, options);
13471}
13472
13473fn divider(ui: &mut UiDocument, parent: UiNodeId, name: &'static str) {
13474 ui.add_child(
13475 parent,
13476 UiNode::container(
13477 name,
13478 LayoutStyle::new()
13479 .with_width_percent(1.0)
13480 .with_height(1.0)
13481 .with_flex_shrink(0.0),
13482 )
13483 .with_visual(UiVisual::panel(color(48, 58, 72), None, 0.0)),
13484 );
13485}
13486
13487fn canvas(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
13488 let canvas_intrinsic = UiSize::new(720.0, 405.0);
13489 let body = section_with_min_viewport(ui, parent, "canvas", "Canvas", UiSize::new(720.0, 458.0));
13490 let controls = wrapping_row(ui, body, "canvas.options", 10.0);
13491 canvas_option_checkbox(
13492 ui,
13493 controls,
13494 "canvas.grow_horizontal",
13495 "Grow width",
13496 state.canvas_grow_horizontal,
13497 );
13498 canvas_option_checkbox(
13499 ui,
13500 controls,
13501 "canvas.grow_vertical",
13502 "Grow height",
13503 state.canvas_grow_vertical,
13504 );
13505 canvas_option_checkbox(
13506 ui,
13507 controls,
13508 "canvas.keep_aspect_ratio",
13509 "Keep aspect ratio",
13510 state.canvas_keep_aspect_ratio,
13511 );
13512
13513 let mut options = widgets::CanvasOptions::default()
13514 .with_accessibility_label("Shader canvas")
13515 .with_action("canvas.rotate")
13516 .with_intrinsic_size(canvas_intrinsic);
13517 options.action_mode = WidgetActionMode::Drag;
13518 if state.canvas_keep_aspect_ratio {
13519 options = options.with_aspect_ratio(16.0 / 9.0);
13520 }
13521 let canvas_width = if state.canvas_grow_horizontal {
13522 LayoutDimension::percent(1.0)
13523 } else {
13524 LayoutDimension::points(canvas_intrinsic.width)
13525 };
13526 let canvas_height = if state.canvas_grow_vertical {
13527 LayoutDimension::percent(1.0)
13528 } else {
13529 LayoutDimension::points(canvas_intrinsic.height)
13530 };
13531 options.layout = Layout::new()
13532 .size(LayoutSize::new(canvas_width, canvas_height))
13533 .min_size(LayoutSize::points(
13534 canvas_intrinsic.width,
13535 canvas_intrinsic.height,
13536 ))
13537 .flex(
13538 if state.canvas_grow_vertical { 1.0 } else { 0.0 },
13539 1.0,
13540 LayoutDimension::Auto,
13541 )
13542 .to_layout_style();
13543 options.visual = UiVisual::panel(
13544 color(18, 22, 28),
13545 Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
13546 4.0,
13547 );
13548 widgets::canvas(
13549 ui,
13550 body,
13551 "canvas.shader",
13552 CanvasContent::new("canvas.shader").program(showcase_canvas_program(state.cube)),
13553 options,
13554 );
13555}
13556
13557fn canvas_option_checkbox(
13558 ui: &mut UiDocument,
13559 parent: UiNodeId,
13560 name: &'static str,
13561 label: &'static str,
13562 checked: bool,
13563) {
13564 let mut options = widgets::CheckboxOptions::default()
13565 .with_action(name)
13566 .with_text_style(text(12.0, color(220, 228, 238)));
13567 options.layout = LayoutStyle::new().with_height(28.0).with_flex_shrink(0.0);
13568 widgets::checkbox(ui, parent, name, label, checked, options);
13569}
13570
13571fn showcase_canvas_program(cube: CanvasCubeState) -> CanvasRenderProgram {
13572 CanvasRenderProgram::wgsl(include_str!("shaders/showcase_canvas.wgsl"))
13573 .label("showcase.canvas")
13574 .constant("CUBE_YAW", cube.yaw as f64)
13575 .constant("CUBE_PITCH", cube.pitch as f64)
13576 .clear_color(Some(color(18, 22, 28)))
13577}
13578
13579fn section(
13580 ui: &mut UiDocument,
13581 parent: UiNodeId,
13582 name: impl Into<String>,
13583 _title: impl Into<String>,
13584) -> UiNodeId {
13585 section_with_min_viewport(ui, parent, name, _title, UiSize::ZERO)
13586}
13587
13588fn section_with_min_viewport(
13589 ui: &mut UiDocument,
13590 parent: UiNodeId,
13591 name: impl Into<String>,
13592 _title: impl Into<String>,
13593 min_viewport_size: UiSize,
13594) -> UiNodeId {
13595 let name = name.into();
13596 let layout = Layout::column()
13597 .size(LayoutSize::percent(1.0, 1.0))
13598 .min_size(LayoutSize::points(
13599 min_viewport_size.width.max(0.0),
13600 min_viewport_size.height.max(0.0),
13601 ))
13602 .gap(LayoutGap::points(10.0, 10.0))
13603 .flex(1.0, 1.0, LayoutDimension::Auto)
13604 .to_layout_style();
13605 widgets::scroll_area(
13606 ui,
13607 parent,
13608 format!("{name}.section_scroll"),
13609 ScrollAxes::BOTH,
13610 layout,
13611 )
13612}
13613
13614fn row(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>, gap: f32) -> UiNodeId {
13615 ui.add_child(
13616 parent,
13617 UiNode::container(
13618 name,
13619 Layout::row()
13620 .size(LayoutSize::new(
13621 LayoutDimension::percent(1.0),
13622 LayoutDimension::Auto,
13623 ))
13624 .align_items(LayoutAlignment::Center)
13625 .gap(LayoutGap::points(gap, gap))
13626 .flex(0.0, 0.0, LayoutDimension::Auto)
13627 .to_layout_style(),
13628 ),
13629 )
13630}
13631
13632fn wrapping_row(
13633 ui: &mut UiDocument,
13634 parent: UiNodeId,
13635 name: impl Into<String>,
13636 gap: f32,
13637) -> UiNodeId {
13638 ui.add_child(
13639 parent,
13640 UiNode::container(
13641 name,
13642 Layout::row()
13643 .size(LayoutSize::new(
13644 LayoutDimension::percent(1.0),
13645 LayoutDimension::Auto,
13646 ))
13647 .min_size(LayoutSize::points(0.0, 0.0))
13648 .align_items(LayoutAlignment::Center)
13649 .gap(LayoutGap::points(gap, gap))
13650 .flex_wrap(LayoutFlexWrap::Wrap)
13651 .flex(0.0, 0.0, LayoutDimension::Auto)
13652 .to_layout_style(),
13653 ),
13654 )
13655}Sourcepub const fn points(width: f32, height: f32) -> Self
pub const fn points(width: f32, height: f32) -> Self
Examples found in repository?
examples/showcase.rs (line 8191)
8163fn list_and_table_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
8164 let body = section_with_min_viewport(
8165 ui,
8166 parent,
8167 "lists_tables",
8168 "Lists and tables",
8169 UiSize::new(520.0, 0.0),
8170 );
8171
8172 let list_row = ui.add_child(
8173 body,
8174 UiNode::container(
8175 "lists_tables.list_row",
8176 Layout::row()
8177 .size(LayoutSize::new(
8178 LayoutDimension::percent(1.0),
8179 LayoutDimension::Auto,
8180 ))
8181 .gap(LayoutGap::points(10.0, 10.0))
8182 .flex_wrap(LayoutFlexWrap::Wrap)
8183 .to_layout_style(),
8184 ),
8185 );
8186 let scroll_column = ui.add_child(
8187 list_row,
8188 UiNode::container(
8189 "lists_tables.scroll_area.column",
8190 Layout::column()
8191 .min_size(LayoutSize::points(220.0, 0.0))
8192 .gap(LayoutGap::points(6.0, 6.0))
8193 .flex(1.0, 1.0, LayoutDimension::points(245.0))
8194 .to_layout_style(),
8195 ),
8196 );
8197 widgets::label(
8198 ui,
8199 scroll_column,
8200 "lists_tables.scroll_area.title",
8201 "Scrollable list",
8202 text(12.0, color(166, 176, 190)),
8203 LayoutStyle::new().with_width_percent(1.0),
8204 );
8205 let nested_scroll = widgets::scroll_area(
8206 ui,
8207 scroll_column,
8208 "lists_tables.scroll_area",
8209 ScrollAxes::VERTICAL,
8210 LayoutStyle::column()
8211 .with_width_percent(1.0)
8212 .with_height(104.0),
8213 );
8214 ui.node_mut(nested_scroll)
8215 .set_action("lists_tables.scroll_area.scroll");
8216 if let Some(scroll) = ui.node_mut(nested_scroll).scroll_mut() {
8217 scroll.set_offset(UiPoint::new(0.0, state.list_scroll));
8218 }
8219 for index in 0..6 {
8220 widgets::label(
8221 ui,
8222 nested_scroll,
8223 format!("lists_tables.scroll_area.row.{index}"),
8224 format!("Scroll row {}", index + 1),
8225 text(12.0, color(200, 212, 228)),
8226 LayoutStyle::new()
8227 .with_width_percent(1.0)
8228 .with_height(26.0)
8229 .with_flex_shrink(0.0),
8230 );
8231 }
8232
8233 let virtual_list_column = ui.add_child(
8234 list_row,
8235 UiNode::container(
8236 "lists_tables.virtual_list.column",
8237 Layout::column()
8238 .min_size(LayoutSize::points(220.0, 0.0))
8239 .gap(LayoutGap::points(6.0, 6.0))
8240 .flex(1.0, 1.0, LayoutDimension::points(245.0))
8241 .to_layout_style(),
8242 ),
8243 );
8244
8245 widgets::label(
8246 ui,
8247 virtual_list_column,
8248 "lists_tables.virtual_list.title",
8249 "Virtualized list",
8250 text(12.0, color(166, 176, 190)),
8251 LayoutStyle::new().with_width_percent(1.0),
8252 );
8253 let virtual_list = widgets::virtual_list(
8254 ui,
8255 virtual_list_column,
8256 "lists_tables.virtual_list",
8257 widgets::VirtualListSpec {
8258 row_count: 24,
8259 row_height: 28.0,
8260 viewport_height: 104.0,
8261 scroll_offset: state.virtual_scroll,
8262 overscan: 1,
8263 },
8264 |ui, row_parent, row| {
8265 widgets::label(
8266 ui,
8267 row_parent,
8268 format!("lists_tables.virtual_list.row.{row}"),
8269 format!("Virtual row {}", row + 1),
8270 text(12.0, color(214, 224, 238)),
8271 LayoutStyle::new()
8272 .with_width_percent(1.0)
8273 .with_height(28.0)
8274 .with_flex_shrink(0.0),
8275 );
8276 },
8277 );
8278 ui.node_mut(virtual_list)
8279 .set_action("lists_tables.virtual_list.scroll");
8280
8281 widgets::separator(
8282 ui,
8283 body,
8284 "lists_tables.virtualized_table.separator",
8285 widgets::SeparatorOptions::default(),
8286 );
8287 widgets::label(
8288 ui,
8289 body,
8290 "lists_tables.data_table.title",
8291 "Virtualized selectable table",
8292 text(12.0, color(166, 176, 190)),
8293 LayoutStyle::new().with_width_percent(1.0),
8294 );
8295 let virtual_controls = wrapping_row(ui, body, "lists_tables.virtualized_table.controls", 8.0);
8296 button(
8297 ui,
8298 virtual_controls,
8299 "lists_tables.virtualized_table.sort.name",
8300 if state.virtual_table_descending {
8301 "Name desc"
8302 } else {
8303 "Name asc"
8304 },
8305 "lists_tables.virtualized_table.sort.name",
8306 button_visual(38, 52, 70),
8307 );
8308 button(
8309 ui,
8310 virtual_controls,
8311 "lists_tables.virtualized_table.filter.status",
8312 if state.virtual_table_ready_only {
8313 "Ready only"
8314 } else {
8315 "All status"
8316 },
8317 "lists_tables.virtualized_table.filter.status",
8318 button_visual(38, 52, 70),
8319 );
8320 button(
8321 ui,
8322 virtual_controls,
8323 "lists_tables.virtualized_table.resize.reset",
8324 "Reset width",
8325 "lists_tables.virtualized_table.resize.reset",
8326 button_visual(38, 52, 70),
8327 );
8328
8329 let columns = virtual_table_columns(state);
8330 let visible_rows = virtual_table_visible_rows(state);
8331 let mut table_options = ext_widgets::DataTableOptions::default()
8332 .with_row_action_prefix("lists_tables.virtualized_table")
8333 .with_cell_action_prefix("lists_tables.virtualized_table")
8334 .with_scroll_action("lists_tables.virtualized_table.scroll");
8335 table_options.layout = LayoutStyle::column()
8336 .with_width_percent(1.0)
8337 .with_flex_shrink(0.0);
8338 table_options.header_visual = UiVisual::panel(
8339 color(34, 41, 50),
8340 Some(StrokeStyle::new(color(67, 78, 95), 1.0)),
8341 0.0,
8342 );
8343 table_options.header_text_style = text(12.0, color(222, 230, 240));
8344 table_options.selection = state.table_selection.clone();
8345 ext_widgets::virtualized_data_table(
8346 ui,
8347 body,
8348 "lists_tables.virtualized_table",
8349 &columns,
8350 ext_widgets::VirtualDataTableSpec {
8351 row_count: visible_rows.len(),
8352 row_height: 28.0,
8353 viewport_width: 520.0,
8354 viewport_height: 156.0,
8355 scroll_offset: UiPoint::new(0.0, state.virtual_table_scroll),
8356 overscan_rows: 1,
8357 },
8358 table_options,
8359 |ui, cell_parent, cell| {
8360 let source_row = visible_rows.get(cell.row).copied().unwrap_or(cell.row);
8361 let value = virtual_table_cell_value(source_row, cell.column);
8362 widgets::label(
8363 ui,
8364 cell_parent,
8365 format!(
8366 "lists_tables.virtualized_table.cell.{}.{}.label",
8367 cell.row, cell.column
8368 ),
8369 value,
8370 text(12.0, color(220, 228, 238)),
8371 LayoutStyle::new().with_width_percent(1.0),
8372 );
8373 },
8374 );
8375}
8376
8377#[allow(clippy::field_reassign_with_default)]
8378fn property_inspector(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
8379 let body = section(ui, parent, "property_inspector", "Property inspector");
8380 widgets::label(
8381 ui,
8382 body,
8383 "property_inspector.target",
8384 "Inspecting: Styling preview",
8385 text(12.0, color(196, 210, 230)),
8386 LayoutStyle::new().with_width_percent(1.0),
8387 );
8388 let mut options = ext_widgets::PropertyInspectorOptions::default();
8389 options.selected_index = Some(0);
8390 options.label_width = 120.0;
8391 options.row_height = 30.0;
8392 ext_widgets::property_inspector_grid(
8393 ui,
8394 body,
8395 "property_inspector.grid",
8396 &[
8397 ext_widgets::PropertyGridRow::new("target", "Widget", "Button preview").read_only(),
8398 ext_widgets::PropertyGridRow::new(
8399 "inner",
8400 "Inner margin",
8401 format!("{:.0}px", state.styling.inner_margin),
8402 )
8403 .with_kind(ext_widgets::PropertyValueKind::Number),
8404 ext_widgets::PropertyGridRow::new(
8405 "outer",
8406 "Outer margin",
8407 format!("{:.0}px", state.styling.outer_margin),
8408 )
8409 .with_kind(ext_widgets::PropertyValueKind::Number),
8410 ext_widgets::PropertyGridRow::new(
8411 "radius",
8412 "Corner radius",
8413 format!("{:.0}px", state.styling.corner_radius),
8414 )
8415 .with_kind(ext_widgets::PropertyValueKind::Number),
8416 ext_widgets::PropertyGridRow::new(
8417 "stroke",
8418 "Stroke",
8419 format!("{:.1}px", state.styling.stroke_width),
8420 )
8421 .with_kind(ext_widgets::PropertyValueKind::Number)
8422 .changed(),
8423 ext_widgets::PropertyGridRow::new("state", "Source", "Styling widget").read_only(),
8424 ],
8425 options,
8426 );
8427}
8428
8429fn diagnostics_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
8430 let body = section(ui, parent, "diagnostics", "Diagnostics");
8431 let debug_snapshot = &state.diagnostics_snapshot;
8432
8433 diagnostics_selected_node_panel(ui, body, debug_snapshot);
8434 diagnostics_animation_panel(ui, body, state, debug_snapshot);
8435
8436 widgets::label(
8437 ui,
8438 body,
8439 "diagnostics.a11y.title",
8440 "Accessibility",
8441 text(14.0, color(222, 230, 240)),
8442 LayoutStyle::new().with_width_percent(1.0),
8443 );
8444 let mut overlay_preview_style = UiNodeStyle::from(
8445 LayoutStyle::new()
8446 .with_width(320.0)
8447 .with_height(140.0)
8448 .with_flex_shrink(0.0),
8449 );
8450 overlay_preview_style.set_clip(ClipBehavior::Clip);
8451 let overlay_preview = ui.add_child(
8452 body,
8453 UiNode::container("diagnostics.a11y.preview", overlay_preview_style).with_visual(
8454 UiVisual::panel(
8455 color(12, 17, 24),
8456 Some(StrokeStyle::new(color(47, 62, 82), 1.0)),
8457 4.0,
8458 ),
8459 ),
8460 );
8461 let mut overlay_options = ext_widgets::AccessibilityDebugOverlayOptions {
8462 action_prefix: Some("diagnostics.a11y.visual".to_owned()),
8463 ..Default::default()
8464 };
8465 overlay_options.show_labels = false;
8466 ext_widgets::accessibility_debug_overlay(
8467 ui,
8468 overlay_preview,
8469 "diagnostics.a11y.visual",
8470 &debug_snapshot,
8471 overlay_options,
8472 );
8473 diagnostics_accessibility_details(ui, body, debug_snapshot);
8474
8475 let diagnostic_columns = ui.add_child(
8476 body,
8477 UiNode::container(
8478 "diagnostics.columns",
8479 LayoutStyle::column()
8480 .with_width_percent(1.0)
8481 .with_flex_shrink(0.0)
8482 .gap(10.0),
8483 ),
8484 );
8485 let command_column = ui.add_child(
8486 diagnostic_columns,
8487 UiNode::container(
8488 "diagnostics.commands.column",
8489 LayoutStyle::column()
8490 .with_width_percent(1.0)
8491 .with_flex_shrink(0.0)
8492 .gap(8.0),
8493 ),
8494 );
8495 let theme_column = ui.add_child(
8496 diagnostic_columns,
8497 UiNode::container(
8498 "diagnostics.theme.column",
8499 LayoutStyle::column()
8500 .with_width_percent(1.0)
8501 .with_flex_shrink(0.0)
8502 .gap(8.0),
8503 ),
8504 );
8505
8506 let registry = diagnostics_command_registry();
8507 diagnostics_commands_panel(ui, command_column, ®istry);
8508
8509 let theme_snapshot = DebugThemeSnapshot::from_theme(&Theme::dark());
8510 diagnostics_theme_panel(ui, theme_column, &theme_snapshot);
8511}
8512
8513fn diagnostics_selected_node_panel(
8514 ui: &mut UiDocument,
8515 parent: UiNodeId,
8516 snapshot: &DebugInspectorSnapshot,
8517) {
8518 let panel = diagnostics_panel(ui, parent, "diagnostics.inspector", "Selected node");
8519 let rows = snapshot
8520 .node("diagnostics.sample.preview")
8521 .map(|node| {
8522 vec![
8523 ext_widgets::PropertyGridRow::new("name", "Node", "Preview action").read_only(),
8524 ext_widgets::PropertyGridRow::new("role", "Role", "Button").read_only(),
8525 ext_widgets::PropertyGridRow::new(
8526 "bounds",
8527 "Bounds",
8528 format!(
8529 "{:.0}, {:.0}; {:.0} x {:.0}",
8530 node.rect.x, node.rect.y, node.rect.width, node.rect.height
8531 ),
8532 )
8533 .with_kind(ext_widgets::PropertyValueKind::Number)
8534 .read_only(),
8535 ext_widgets::PropertyGridRow::new(
8536 "clip",
8537 "Clip",
8538 format!("{:.0} x {:.0}", node.clip_rect.width, node.clip_rect.height),
8539 )
8540 .with_kind(ext_widgets::PropertyValueKind::Number)
8541 .read_only(),
8542 ext_widgets::PropertyGridRow::new(
8543 "input",
8544 "Input",
8545 if node.input.pointer {
8546 "Receives pointer input"
8547 } else {
8548 "Passive"
8549 },
8550 )
8551 .read_only(),
8552 ]
8553 })
8554 .unwrap_or_else(|| {
8555 vec![
8556 ext_widgets::PropertyGridRow::new("missing", "Selected node", "No node selected")
8557 .read_only(),
8558 ]
8559 });
8560 ext_widgets::property_inspector_grid(
8561 ui,
8562 panel,
8563 "diagnostics.inspector.rows",
8564 &rows,
8565 diagnostics_grid_options("Selected node details"),
8566 );
8567}
8568
8569fn diagnostics_animation_panel(
8570 ui: &mut UiDocument,
8571 parent: UiNodeId,
8572 state: &ShowcaseState,
8573 snapshot: &DebugInspectorSnapshot,
8574) {
8575 let graph_panel =
8576 diagnostics_panel(ui, parent, "diagnostics.animation.graph", "Animation state");
8577 if let Some(animation) = snapshot.animation("diagnostics.sample.preview") {
8578 let state_row = row(ui, graph_panel, "diagnostics.animation.graph.states", 8.0);
8579 for state_name in ["idle", "hot"] {
8580 diagnostic_chip(
8581 ui,
8582 state_row,
8583 format!("diagnostics.animation.graph.state.{state_name}"),
8584 state_name,
8585 animation.current_state == state_name,
8586 );
8587 }
8588
8589 let graph = animation.state_graph();
8590 for (index, edge) in graph.edges.iter().take(2).enumerate() {
8591 let value = if edge.kind == DebugAnimationGraphEdgeKind::Blend {
8592 "Input blend"
8593 } else {
8594 "State change"
8595 };
8596 let detail = if edge.label.is_empty() {
8597 if edge.active { "Active" } else { "Inactive" }.to_owned()
8598 } else if edge.active {
8599 format!("{}; active", edge.label)
8600 } else {
8601 edge.label.clone()
8602 };
8603 diagnostic_value_row(
8604 ui,
8605 graph_panel,
8606 format!("diagnostics.animation.graph.edge.{index}"),
8607 value,
8608 format!("{} -> {}", edge.from, edge.to),
8609 );
8610 diagnostic_muted_label(
8611 ui,
8612 graph_panel,
8613 format!("diagnostics.animation.graph.edge.{index}.detail"),
8614 detail,
8615 );
8616 }
8617 } else {
8618 diagnostic_muted_label(
8619 ui,
8620 graph_panel,
8621 "diagnostics.animation.graph.empty",
8622 "No animation state machine",
8623 );
8624 }
8625
8626 let controls_panel = diagnostics_panel(
8627 ui,
8628 parent,
8629 "diagnostics.animation.controls",
8630 "Animation controls",
8631 );
8632 let transport = row(
8633 ui,
8634 controls_panel,
8635 "diagnostics.animation.controls.transport",
8636 8.0,
8637 );
8638 diagnostic_button(
8639 ui,
8640 transport,
8641 "diagnostics.animation.controls.transport.pause_toggle",
8642 if state.diagnostics_animation_paused {
8643 "Resume"
8644 } else {
8645 "Pause"
8646 },
8647 state.diagnostics_animation_paused,
8648 );
8649 diagnostic_button(
8650 ui,
8651 transport,
8652 "diagnostics.animation.controls.transport.step",
8653 "Step",
8654 false,
8655 );
8656 diagnostic_slider_row(
8657 ui,
8658 controls_panel,
8659 "diagnostics.animation.controls.transport.scrub",
8660 "Scrub progress",
8661 state.diagnostics_animation_scrub,
8662 "diagnostics.animation.controls.transport.scrub",
8663 );
8664 diagnostic_button(
8665 ui,
8666 controls_panel,
8667 "diagnostics.animation.controls.input.active.toggle",
8668 if state.diagnostics_animation_active {
8669 "Active input: true"
8670 } else {
8671 "Active input: false"
8672 },
8673 state.diagnostics_animation_active,
8674 );
8675 diagnostic_slider_row(
8676 ui,
8677 controls_panel,
8678 "diagnostics.animation.controls.input.hover.set",
8679 "Hover blend",
8680 state.diagnostics_animation_hover,
8681 "diagnostics.animation.controls.input.hover.set",
8682 );
8683 diagnostic_button(
8684 ui,
8685 controls_panel,
8686 "diagnostics.animation.controls.input.pulse.fire",
8687 "Fire pulse",
8688 false,
8689 );
8690 widgets::label(
8691 ui,
8692 controls_panel,
8693 "diagnostics.animation.controls.status",
8694 format!(
8695 "Scrub {:.0}% Hover {:.0}% Pulses {}",
8696 state.diagnostics_animation_scrub * 100.0,
8697 state.diagnostics_animation_hover * 100.0,
8698 state.diagnostics_animation_pulse_count
8699 ),
8700 text(12.0, color(166, 180, 198)),
8701 LayoutStyle::new().with_width_percent(1.0),
8702 );
8703}
8704
8705fn diagnostics_accessibility_details(
8706 ui: &mut UiDocument,
8707 parent: UiNodeId,
8708 snapshot: &DebugInspectorSnapshot,
8709) {
8710 let rows = snapshot
8711 .accessibility_overlay
8712 .iter()
8713 .find(|node| node.name == "diagnostics.sample.preview")
8714 .map(|node| {
8715 let accessibility = node.accessibility.as_ref();
8716 vec![
8717 ext_widgets::PropertyGridRow::new("role", "Role", "Button").read_only(),
8718 ext_widgets::PropertyGridRow::new(
8719 "label",
8720 "Label",
8721 accessibility
8722 .and_then(|meta| meta.label.clone())
8723 .unwrap_or_else(|| "Preview action".to_owned()),
8724 )
8725 .read_only(),
8726 ext_widgets::PropertyGridRow::new(
8727 "focus",
8728 "Focus order",
8729 node.focus_index
8730 .map(|index| format!("#{}", index + 1))
8731 .unwrap_or_else(|| "Not focusable".to_owned()),
8732 )
8733 .read_only(),
8734 ext_widgets::PropertyGridRow::new(
8735 "warnings",
8736 "Warnings",
8737 if node.warnings.is_empty() {
8738 "None"
8739 } else {
8740 "Needs review"
8741 },
8742 )
8743 .read_only(),
8744 ]
8745 })
8746 .unwrap_or_else(|| {
8747 vec![
8748 ext_widgets::PropertyGridRow::new("missing", "Accessibility", "No metadata")
8749 .read_only(),
8750 ]
8751 });
8752 ext_widgets::property_inspector_grid(
8753 ui,
8754 parent,
8755 "diagnostics.a11y",
8756 &rows,
8757 diagnostics_grid_options("Accessibility metadata"),
8758 );
8759}
8760
8761fn diagnostics_commands_panel(ui: &mut UiDocument, parent: UiNodeId, registry: &CommandRegistry) {
8762 let panel = diagnostics_panel(ui, parent, "diagnostics.commands", "Commands");
8763 let formatter = ShortcutFormatter::default();
8764 for command_id in [
8765 "diagnostics.palette",
8766 "diagnostics.inspect",
8767 "diagnostics.record",
8768 "diagnostics.export_theme",
8769 ] {
8770 if let Some(command) = registry.command(command_id) {
8771 let shortcut = registry
8772 .command_bindings(command.meta.id.clone())
8773 .first()
8774 .map(|binding| formatter.format(binding.shortcut))
8775 .unwrap_or_else(|| "Unbound".to_owned());
8776 let status = if command.enabled {
8777 command
8778 .meta
8779 .category
8780 .clone()
8781 .unwrap_or_else(|| "General".to_owned())
8782 } else {
8783 command
8784 .disabled_reason
8785 .clone()
8786 .unwrap_or_else(|| "Disabled".to_owned())
8787 };
8788 diagnostic_command_row(
8789 ui,
8790 panel,
8791 format!(
8792 "diagnostics.commands.row.{}",
8793 command.meta.id.as_str().replace('.', "_")
8794 ),
8795 &command.meta.label,
8796 &shortcut,
8797 &status,
8798 );
8799 }
8800 }
8801 diagnostic_value_row(
8802 ui,
8803 panel,
8804 "diagnostics.commands.conflicts",
8805 "Shortcut conflicts",
8806 if registry.conflicts().is_empty() {
8807 "None"
8808 } else {
8809 "Needs review"
8810 },
8811 );
8812}
8813
8814fn diagnostics_theme_panel(ui: &mut UiDocument, parent: UiNodeId, snapshot: &DebugThemeSnapshot) {
8815 let panel = diagnostics_panel(ui, parent, "diagnostics.theme", "Theme tokens");
8816 diagnostic_value_row(
8817 ui,
8818 panel,
8819 "diagnostics.theme.name",
8820 "Theme",
8821 snapshot.name.as_str(),
8822 );
8823 for token_path in ["colors.accent", "colors.surface", "typography.body"] {
8824 if let Some(token) = snapshot.token(token_path) {
8825 diagnostic_value_row(
8826 ui,
8827 panel,
8828 format!("diagnostics.theme.token.{}", token_path.replace('.', "_")),
8829 token_path,
8830 token.value.as_str(),
8831 );
8832 }
8833 }
8834 if let Some(component) = snapshot.component_states.first() {
8835 diagnostic_value_row(
8836 ui,
8837 panel,
8838 "diagnostics.theme.component.button",
8839 "Button normal",
8840 format!(
8841 "{:.0} x {:.0}, padding {:.0}",
8842 component.min_width, component.min_height, component.padding_x
8843 ),
8844 );
8845 }
8846}
8847
8848fn diagnostics_panel(
8849 ui: &mut UiDocument,
8850 parent: UiNodeId,
8851 name: impl Into<String>,
8852 title: impl Into<String>,
8853) -> UiNodeId {
8854 let name = name.into();
8855 let title = title.into();
8856 let panel = ui.add_child(
8857 parent,
8858 UiNode::container(
8859 name.clone(),
8860 LayoutStyle::column()
8861 .with_width_percent(1.0)
8862 .with_padding(10.0)
8863 .with_gap(8.0)
8864 .with_flex_shrink(0.0),
8865 )
8866 .with_visual(UiVisual::panel(
8867 color(15, 20, 28),
8868 Some(StrokeStyle::new(color(52, 65, 84), 1.0)),
8869 4.0,
8870 ))
8871 .with_accessibility(AccessibilityMeta::new(AccessibilityRole::Group).label(title.clone())),
8872 );
8873 widgets::label(
8874 ui,
8875 panel,
8876 format!("{name}.title"),
8877 title,
8878 text(13.0, color(222, 230, 240)),
8879 LayoutStyle::new().with_width_percent(1.0),
8880 );
8881 panel
8882}
8883
8884fn diagnostics_grid_options(label: impl Into<String>) -> ext_widgets::PropertyInspectorOptions {
8885 ext_widgets::PropertyInspectorOptions {
8886 label_width: 112.0,
8887 row_height: 28.0,
8888 accessibility_label: Some(label.into()),
8889 ..Default::default()
8890 }
8891}
8892
8893fn diagnostic_value_row(
8894 ui: &mut UiDocument,
8895 parent: UiNodeId,
8896 name: impl Into<String>,
8897 label: impl Into<String>,
8898 value: impl Into<String>,
8899) -> UiNodeId {
8900 let name = name.into();
8901 let row = row(ui, parent, name.clone(), 8.0);
8902 widgets::label(
8903 ui,
8904 row,
8905 format!("{name}.label"),
8906 label.into(),
8907 text(12.0, color(166, 180, 198)),
8908 LayoutStyle::new().with_width(136.0).with_flex_shrink(0.0),
8909 );
8910 widgets::label(
8911 ui,
8912 row,
8913 format!("{name}.value"),
8914 value.into(),
8915 text(12.0, color(226, 234, 244)),
8916 LayoutStyle::new().with_width_percent(1.0),
8917 );
8918 row
8919}
8920
8921fn diagnostic_muted_label(
8922 ui: &mut UiDocument,
8923 parent: UiNodeId,
8924 name: impl Into<String>,
8925 label: impl Into<String>,
8926) -> UiNodeId {
8927 let mut style = text(12.0, color(166, 180, 198));
8928 style.wrap = TextWrap::WordOrGlyph;
8929 widgets::label(
8930 ui,
8931 parent,
8932 name,
8933 label.into(),
8934 style,
8935 LayoutStyle::new().with_width_percent(1.0),
8936 )
8937}
8938
8939fn diagnostic_command_row(
8940 ui: &mut UiDocument,
8941 parent: UiNodeId,
8942 name: impl Into<String>,
8943 label: &str,
8944 shortcut: &str,
8945 status: &str,
8946) -> UiNodeId {
8947 let name = name.into();
8948 let row = row(ui, parent, name.clone(), 8.0);
8949 widgets::label(
8950 ui,
8951 row,
8952 format!("{name}.label"),
8953 label,
8954 text(12.0, color(226, 234, 244)),
8955 LayoutStyle::new()
8956 .with_width_percent(1.0)
8957 .with_flex_grow(1.0),
8958 );
8959 widgets::label(
8960 ui,
8961 row,
8962 format!("{name}.shortcut"),
8963 shortcut,
8964 text(12.0, color(166, 180, 198)),
8965 LayoutStyle::new().with_width(78.0).with_flex_shrink(0.0),
8966 );
8967 widgets::label(
8968 ui,
8969 row,
8970 format!("{name}.status"),
8971 status,
8972 text(12.0, color(166, 180, 198)),
8973 LayoutStyle::new().with_width(140.0).with_flex_shrink(0.0),
8974 );
8975 row
8976}
8977
8978fn diagnostic_slider_row(
8979 ui: &mut UiDocument,
8980 parent: UiNodeId,
8981 name: impl Into<String>,
8982 label: impl Into<String>,
8983 value: f32,
8984 action: impl Into<String>,
8985) -> UiNodeId {
8986 let name = name.into();
8987 let label = label.into();
8988 let row = row(ui, parent, format!("{name}.row"), 8.0);
8989 widgets::label(
8990 ui,
8991 row,
8992 format!("{name}.label"),
8993 label.clone(),
8994 text(12.0, color(166, 180, 198)),
8995 LayoutStyle::new().with_width(136.0).with_flex_shrink(0.0),
8996 );
8997 let slider_name = if name.ends_with(".set") {
8998 format!("{name}.slider")
8999 } else {
9000 name.clone()
9001 };
9002 let mut options = widgets::SliderOptions::default()
9003 .with_layout(LayoutStyle::new().with_width(160.0).with_height(24.0))
9004 .with_value_edit_action(action.into());
9005 options.accessibility_label = Some(label);
9006 widgets::slider(ui, row, slider_name, value, 0.0..1.0, options);
9007 widgets::label(
9008 ui,
9009 row,
9010 format!("{name}.percent"),
9011 format!("{:.0}%", value * 100.0),
9012 text(12.0, color(226, 234, 244)),
9013 LayoutStyle::new().with_width(46.0).with_flex_shrink(0.0),
9014 );
9015 row
9016}
9017
9018fn diagnostic_button(
9019 ui: &mut UiDocument,
9020 parent: UiNodeId,
9021 name: impl Into<String>,
9022 label: impl Into<String>,
9023 active: bool,
9024) -> UiNodeId {
9025 let name = name.into();
9026 let mut options = widgets::ButtonOptions::default()
9027 .with_layout(LayoutStyle::new().with_height(32.0))
9028 .with_action(name.clone())
9029 .pressed(active);
9030 if active {
9031 options.visual = UiVisual::panel(
9032 color(47, 94, 150),
9033 Some(StrokeStyle::new(color(103, 164, 224), 1.0)),
9034 4.0,
9035 );
9036 }
9037 widgets::button(ui, parent, name, label, options)
9038}
9039
9040fn diagnostic_chip(
9041 ui: &mut UiDocument,
9042 parent: UiNodeId,
9043 name: impl Into<String>,
9044 label: impl Into<String>,
9045 active: bool,
9046) -> UiNodeId {
9047 let name = name.into();
9048 let chip = ui.add_child(
9049 parent,
9050 UiNode::container(
9051 name.clone(),
9052 LayoutStyle::new()
9053 .with_width(82.0)
9054 .with_height(28.0)
9055 .with_padding(4.0)
9056 .with_flex_shrink(0.0),
9057 )
9058 .with_visual(if active {
9059 UiVisual::panel(
9060 color(47, 94, 150),
9061 Some(StrokeStyle::new(color(103, 164, 224), 1.0)),
9062 4.0,
9063 )
9064 } else {
9065 UiVisual::panel(
9066 color(31, 39, 50),
9067 Some(StrokeStyle::new(color(62, 76, 96), 1.0)),
9068 4.0,
9069 )
9070 }),
9071 );
9072 widgets::label(
9073 ui,
9074 chip,
9075 format!("{name}.label"),
9076 label.into(),
9077 text(12.0, color(226, 234, 244)),
9078 LayoutStyle::new().with_width_percent(1.0),
9079 );
9080 chip
9081}
9082
9083fn diagnostics_sample_snapshot(state: &ShowcaseState) -> DebugInspectorSnapshot {
9084 diagnostics_sample_snapshot_for(
9085 state.diagnostics_animation_hover,
9086 state.diagnostics_animation_active,
9087 )
9088}
9089
9090fn diagnostics_sample_snapshot_for(hover: f32, active: bool) -> DebugInspectorSnapshot {
9091 let mut sample = UiDocument::new(root_style(320.0, 180.0));
9092 let card = sample.add_child(
9093 sample.root(),
9094 UiNode::container(
9095 "diagnostics.sample.card",
9096 LayoutStyle::column()
9097 .with_width_percent(1.0)
9098 .with_height(120.0)
9099 .padding(12.0)
9100 .gap(8.0),
9101 )
9102 .with_visual(UiVisual::panel(
9103 color(16, 22, 30),
9104 Some(StrokeStyle::new(color(62, 77, 98), 1.0)),
9105 6.0,
9106 ))
9107 .with_accessibility(
9108 AccessibilityMeta::new(AccessibilityRole::Group).label("Diagnostics sample"),
9109 ),
9110 );
9111 sample.add_child(
9112 card,
9113 UiNode::container(
9114 "diagnostics.sample.preview",
9115 LayoutStyle::new().with_width(160.0).with_height(38.0),
9116 )
9117 .with_input(InputBehavior::BUTTON)
9118 .with_visual(UiVisual::panel(
9119 color(52, 112, 180),
9120 Some(StrokeStyle::new(color(116, 183, 255), 1.0)),
9121 5.0,
9122 ))
9123 .with_accessibility(
9124 AccessibilityMeta::new(AccessibilityRole::Button)
9125 .label("Preview action")
9126 .focusable(),
9127 )
9128 .with_animation(
9129 AnimationMachine::new(
9130 vec![
9131 AnimationState::new(
9132 "idle",
9133 AnimatedValues::new(1.0, UiPoint::new(0.0, 0.0), 1.0),
9134 ),
9135 AnimationState::new(
9136 "hot",
9137 AnimatedValues::new(0.92, UiPoint::new(18.0, 0.0), 1.08),
9138 ),
9139 ],
9140 vec![AnimationTransition::when(
9141 "idle",
9142 "hot",
9143 AnimationCondition::bool("active", true),
9144 0.18,
9145 )],
9146 "idle",
9147 )
9148 .expect("sample animation")
9149 .with_number_input("hover", hover)
9150 .with_blend_binding(AnimationBlendBinding::new("hover", "idle", "hot"))
9151 .with_bool_input("active", active)
9152 .with_trigger_input("pulse"),
9153 ),
9154 );
9155 widgets::label(
9156 &mut sample,
9157 card,
9158 "diagnostics.sample.label",
9159 "Sample node",
9160 text(12.0, color(198, 210, 226)),
9161 LayoutStyle::new().with_width_percent(1.0),
9162 );
9163 sample
9164 .compute_layout(UiSize::new(320.0, 180.0), &mut ApproxTextMeasurer)
9165 .expect("sample layout");
9166 DebugInspectorSnapshot::from_document(&sample, &mut ApproxTextMeasurer)
9167}
9168
9169fn diagnostics_command_registry() -> CommandRegistry {
9170 let mut registry = CommandRegistry::new();
9171 registry
9172 .register(
9173 CommandMeta::new("diagnostics.palette", "Open command palette")
9174 .description("Show command search")
9175 .category("Debug"),
9176 )
9177 .expect("command");
9178 registry
9179 .register(
9180 CommandMeta::new("diagnostics.inspect", "Inspect selected node")
9181 .description("Focus the layout inspector")
9182 .category("Debug"),
9183 )
9184 .expect("command");
9185 registry
9186 .register(
9187 CommandMeta::new("diagnostics.record", "Start interaction recording")
9188 .description("Capture replay steps")
9189 .category("Testing"),
9190 )
9191 .expect("command");
9192 registry
9193 .register(CommandMeta::new(
9194 "diagnostics.export_theme",
9195 "Export theme patch",
9196 ))
9197 .expect("command");
9198 registry
9199 .bind_shortcut(
9200 CommandScope::Global,
9201 Shortcut::ctrl('k'),
9202 "diagnostics.palette",
9203 )
9204 .expect("shortcut");
9205 registry
9206 .bind_shortcut(
9207 CommandScope::Panel,
9208 Shortcut::ctrl('i'),
9209 "diagnostics.inspect",
9210 )
9211 .expect("shortcut");
9212 registry
9213 .bind_shortcut(
9214 CommandScope::Panel,
9215 Shortcut::ctrl('r'),
9216 "diagnostics.record",
9217 )
9218 .expect("shortcut");
9219 registry
9220 .disable("diagnostics.export_theme", "No changes to export")
9221 .expect("disable");
9222 registry
9223}
9224
9225fn tree_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9226 let body = section(ui, parent, "trees", "Tree view");
9227 widgets::label(
9228 ui,
9229 body,
9230 "trees.tree_view.title",
9231 "Editable tree",
9232 text(12.0, color(166, 176, 190)),
9233 LayoutStyle::new().with_width_percent(1.0),
9234 );
9235 ext_widgets::tree_view(
9236 ui,
9237 body,
9238 "trees.tree_view",
9239 &editable_tree_items(&state.editable_tree),
9240 &state.tree,
9241 ext_widgets::TreeViewOptions::default().with_row_action_prefix("trees.tree"),
9242 );
9243 widgets::label(
9244 ui,
9245 body,
9246 "trees.editable.status",
9247 &state.editable_tree_status,
9248 text(12.0, color(154, 166, 184)),
9249 LayoutStyle::new().with_width_percent(1.0),
9250 );
9251 widgets::label(
9252 ui,
9253 body,
9254 "trees.virtual.title",
9255 "Virtualized tree",
9256 text(12.0, color(166, 176, 190)),
9257 LayoutStyle::new().with_width_percent(1.0),
9258 );
9259 let virtual_nodes = ext_widgets::virtualized_tree_view(
9260 ui,
9261 body,
9262 "trees.virtual",
9263 &virtual_tree_items(),
9264 &state.tree_virtual,
9265 ext_widgets::VirtualTreeViewSpec::new(24.0, 112.0)
9266 .scroll_offset(state.tree_virtual_scroll)
9267 .overscan_rows(1),
9268 ext_widgets::TreeViewOptions::default().with_row_action_prefix("trees.virtual"),
9269 );
9270 ui.node_mut(virtual_nodes.body)
9271 .set_action("trees.virtual.scroll");
9272 widgets::label(
9273 ui,
9274 body,
9275 "trees.table.title",
9276 "Tree table",
9277 text(12.0, color(166, 176, 190)),
9278 LayoutStyle::new().with_width_percent(1.0),
9279 );
9280 tree_table_widgets(ui, body, state);
9281}
9282
9283fn tree_table_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9284 let rows = state.tree_table.visible_items(&tree_table_items());
9285 let columns = [
9286 ext_widgets::DataTableColumn::new("name", "Name", 220.0),
9287 ext_widgets::DataTableColumn::new("kind", "Kind", 84.0),
9288 ext_widgets::DataTableColumn::new("status", "Status", 92.0),
9289 ];
9290 let mut options = ext_widgets::DataTableOptions::default()
9291 .with_row_action_prefix("trees.table")
9292 .with_cell_action_prefix("trees.table")
9293 .with_scroll_action("trees.table.scroll");
9294 options.selection = state
9295 .tree_table
9296 .selected_index()
9297 .map(ext_widgets::DataTableSelection::single_row)
9298 .unwrap_or_default();
9299 options.layout = LayoutStyle::column()
9300 .with_width_percent(1.0)
9301 .with_height(132.0)
9302 .with_flex_shrink(0.0);
9303 ext_widgets::virtualized_data_table(
9304 ui,
9305 parent,
9306 "trees.table",
9307 &columns,
9308 ext_widgets::VirtualDataTableSpec {
9309 row_count: rows.len(),
9310 row_height: 24.0,
9311 viewport_width: 396.0,
9312 viewport_height: 96.0,
9313 scroll_offset: UiPoint::new(0.0, state.tree_table_scroll),
9314 overscan_rows: 1,
9315 },
9316 options,
9317 |ui, cell_parent, cell| {
9318 let Some(item) = rows.get(cell.row) else {
9319 return;
9320 };
9321 if cell.column == 0 {
9322 tree_table_name_cell(ui, cell_parent, cell.row, item);
9323 } else {
9324 widgets::label(
9325 ui,
9326 cell_parent,
9327 format!("trees.table.cell.{}.{}.label", cell.row, cell.column),
9328 tree_table_cell_value(item, cell.column),
9329 text(12.0, color(220, 228, 238)),
9330 LayoutStyle::new().with_width_percent(1.0),
9331 );
9332 }
9333 },
9334 );
9335}
9336
9337fn tree_table_name_cell(
9338 ui: &mut UiDocument,
9339 parent: UiNodeId,
9340 row: usize,
9341 item: &ext_widgets::TreeVisibleItem,
9342) {
9343 if item.depth > 0 {
9344 ui.add_child(
9345 parent,
9346 UiNode::container(
9347 format!("trees.table.row.{}.indent", item.id),
9348 LayoutStyle::new()
9349 .with_width(item.depth as f32 * 16.0)
9350 .with_height_percent(1.0)
9351 .with_flex_shrink(0.0),
9352 ),
9353 );
9354 }
9355 widgets::label(
9356 ui,
9357 parent,
9358 format!("trees.table.row.{}.disclosure", item.id),
9359 if item.has_children() {
9360 if item.expanded {
9361 "v"
9362 } else {
9363 ">"
9364 }
9365 } else {
9366 ""
9367 },
9368 text(12.0, color(166, 176, 190)),
9369 LayoutStyle::new()
9370 .with_width(18.0)
9371 .with_height_percent(1.0)
9372 .with_flex_shrink(0.0),
9373 );
9374 widgets::label(
9375 ui,
9376 parent,
9377 format!("trees.table.cell.{row}.0.label"),
9378 item.label.clone(),
9379 if item.disabled {
9380 text(12.0, color(154, 166, 184))
9381 } else {
9382 text(12.0, color(220, 228, 238))
9383 },
9384 LayoutStyle::new().with_width_percent(1.0),
9385 );
9386}
9387
9388fn tree_table_cell_value(item: &ext_widgets::TreeVisibleItem, column: usize) -> String {
9389 match column {
9390 0 => item.label.clone(),
9391 1 => {
9392 if item.has_children() {
9393 "Folder".to_owned()
9394 } else {
9395 "File".to_owned()
9396 }
9397 }
9398 _ => {
9399 if item.disabled {
9400 "Locked".to_owned()
9401 } else if item.has_children() && item.expanded {
9402 "Expanded".to_owned()
9403 } else if item.has_children() {
9404 "Collapsed".to_owned()
9405 } else if item.expanded {
9406 "Expanded".to_owned()
9407 } else {
9408 "Ready".to_owned()
9409 }
9410 }
9411 }
9412}
9413
9414fn tab_split_dock_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9415 let body = section_with_min_viewport(
9416 ui,
9417 parent,
9418 "layout_widgets",
9419 "Layout widgets",
9420 UiSize::new(640.0, 360.0),
9421 );
9422 let shell = ui.add_child(
9423 body,
9424 UiNode::container(
9425 "layout_widgets.dock_shell",
9426 LayoutStyle::column()
9427 .with_width_percent(1.0)
9428 .with_height(360.0)
9429 .with_flex_shrink(0.0),
9430 )
9431 .with_visual(UiVisual::panel(
9432 color(13, 17, 23),
9433 Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
9434 0.0,
9435 )),
9436 );
9437
9438 let mut panels = base_layout_dock_panels();
9439 state.layout_dock.apply_order_to_panels(&mut panels);
9440 state.layout_dock.apply_visibility_to_panels(&mut panels);
9441
9442 let mut drawer_options = ext_widgets::DockDrawerRailOptions::default();
9443 drawer_options.layout = LayoutStyle::row()
9444 .with_width_percent(1.0)
9445 .with_height(34.0)
9446 .with_padding(4.0)
9447 .with_gap(4.0);
9448 ext_widgets::dock_drawer_rail(
9449 ui,
9450 shell,
9451 "layout_widgets.dock.drawers",
9452 &[
9453 ext_widgets::DockDrawerDescriptor::new(
9454 "panel_a",
9455 "Panel A",
9456 "panel_a",
9457 ext_widgets::DockSide::Left,
9458 )
9459 .open(!state.layout_dock.is_hidden("panel_a"))
9460 .with_action("layout_widgets.drawer.panel_a"),
9461 ext_widgets::DockDrawerDescriptor::new(
9462 "panel_b",
9463 "Panel B",
9464 "panel_b",
9465 ext_widgets::DockSide::Right,
9466 )
9467 .open(!state.layout_dock.is_hidden("panel_b"))
9468 .with_action("layout_widgets.drawer.panel_b"),
9469 ],
9470 drawer_options,
9471 );
9472
9473 let mut options = ext_widgets::DockWorkspaceOptions::default();
9474 options.layout = LayoutStyle::column()
9475 .with_width_percent(1.0)
9476 .with_height(0.0)
9477 .with_flex_grow(1.0);
9478 options.show_titles = true;
9479 options.handle_thickness = 2.0;
9480 options.panel_visual = UiVisual::panel(
9481 color(18, 22, 29),
9482 Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
9483 0.0,
9484 );
9485 options.center_visual = UiVisual::panel(
9486 color(15, 19, 25),
9487 Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
9488 0.0,
9489 );
9490 options.resize_handle_visual = UiVisual::panel(color(65, 78, 96), None, 0.0);
9491
9492 ext_widgets::dock_workspace(
9493 ui,
9494 shell,
9495 "layout_widgets.dock",
9496 &panels,
9497 options,
9498 |ui, parent, panel| match panel.id.as_str() {
9499 "panel_a" => layout_panel_contents(
9500 ui,
9501 parent,
9502 "layout.panel_a",
9503 state.layout_panel_a_scroll,
9504 &["Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6"],
9505 ),
9506 "workspace" => layout_workspace_contents(
9507 ui,
9508 parent,
9509 "layout.workspace",
9510 state.layout_workspace_scroll,
9511 ),
9512 "panel_b" => layout_panel_contents(
9513 ui,
9514 parent,
9515 "layout.panel_b",
9516 state.layout_panel_b_scroll,
9517 &[
9518 "Value A", "Value B", "Value C", "Value D", "Value E", "Value F",
9519 ],
9520 ),
9521 _ => {}
9522 },
9523 );
9524
9525 if let Some(floating) = state.layout_dock.floating_panel("panel_a") {
9526 let floating_panel = ui.add_child(
9527 shell,
9528 UiNode::container(
9529 "layout_widgets.floating.panel_a",
9530 operad::layout::absolute(
9531 floating.rect.x,
9532 floating.rect.y,
9533 floating.rect.width,
9534 floating.rect.height,
9535 ),
9536 )
9537 .with_visual(UiVisual::panel(
9538 color(18, 22, 29),
9539 Some(StrokeStyle::new(color(86, 102, 124), 1.0)),
9540 4.0,
9541 )),
9542 );
9543 layout_panel_contents(
9544 ui,
9545 floating_panel,
9546 "layout.panel_a_floating",
9547 state.layout_panel_a_scroll,
9548 &["Item 1", "Item 2", "Item 3", "Item 4"],
9549 );
9550 }
9551}
9552
9553fn base_layout_dock_panels() -> Vec<ext_widgets::DockPanelDescriptor> {
9554 vec![
9555 ext_widgets::DockPanelDescriptor::new(
9556 "panel_a",
9557 "Panel A",
9558 ext_widgets::DockSide::Left,
9559 200.0,
9560 )
9561 .with_min_size(150.0)
9562 .resizable(true),
9563 ext_widgets::DockPanelDescriptor::center("workspace", "Workspace").with_min_size(220.0),
9564 ext_widgets::DockPanelDescriptor::new(
9565 "panel_b",
9566 "Panel B",
9567 ext_widgets::DockSide::Right,
9568 200.0,
9569 )
9570 .with_min_size(150.0)
9571 .resizable(true),
9572 ]
9573}
9574
9575fn container_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9576 let body = section_with_min_viewport(
9577 ui,
9578 parent,
9579 "containers",
9580 "Containers",
9581 UiSize::new(420.0, 0.0),
9582 );
9583
9584 let frame = widgets::frame(
9585 ui,
9586 body,
9587 "containers.frame",
9588 widgets::FrameOptions::default().with_layout(
9589 LayoutStyle::column()
9590 .with_width_percent(1.0)
9591 .with_height(64.0)
9592 .with_padding(8.0)
9593 .with_gap(6.0),
9594 ),
9595 );
9596 widgets::strong_label(
9597 ui,
9598 frame,
9599 "containers.frame.title",
9600 "Frame",
9601 LayoutStyle::new().with_width_percent(1.0),
9602 );
9603 widgets::weak_label(
9604 ui,
9605 frame,
9606 "containers.frame.body",
9607 "Framed surface with padding.",
9608 LayoutStyle::new().with_width_percent(1.0),
9609 );
9610
9611 let group = widgets::group(ui, body, "containers.group");
9612 widgets::label(
9613 ui,
9614 group,
9615 "containers.group.label",
9616 "Group helper",
9617 text(12.0, color(220, 228, 238)),
9618 LayoutStyle::new().with_width_percent(1.0),
9619 );
9620 let generic_panel = widgets::panel(
9621 ui,
9622 body,
9623 "containers.panel",
9624 widgets::PanelOptions::group().with_layout(
9625 LayoutStyle::column()
9626 .with_width_percent(1.0)
9627 .with_height(44.0)
9628 .with_padding(8.0),
9629 ),
9630 );
9631 widgets::label(
9632 ui,
9633 generic_panel,
9634 "containers.panel.label",
9635 "Generic panel",
9636 text(12.0, color(220, 228, 238)),
9637 LayoutStyle::new().with_width_percent(1.0),
9638 );
9639 let group_panel = widgets::group_panel(ui, body, "containers.group_panel");
9640 widgets::label(
9641 ui,
9642 group_panel,
9643 "containers.group_panel.label",
9644 "Group panel",
9645 text(12.0, color(220, 228, 238)),
9646 LayoutStyle::new().with_width_percent(1.0),
9647 );
9648
9649 widgets::separator(
9650 ui,
9651 body,
9652 "containers.separator",
9653 widgets::SeparatorOptions::default(),
9654 );
9655 widgets::spacer(
9656 ui,
9657 body,
9658 "containers.spacer",
9659 LayoutStyle::new()
9660 .with_width_percent(1.0)
9661 .with_height(8.0)
9662 .with_flex_shrink(0.0),
9663 );
9664
9665 let grid = widgets::grid::grid(
9666 ui,
9667 body,
9668 "containers.grid",
9669 widgets::grid::GridOptions::default().with_layout(
9670 LayoutStyle::column()
9671 .with_width_percent(1.0)
9672 .with_height(78.0)
9673 .with_gap(4.0),
9674 ),
9675 );
9676 for row_index in 0..2 {
9677 let row = widgets::grid::grid_row(
9678 ui,
9679 grid,
9680 format!("containers.grid.row.{row_index}"),
9681 widgets::grid::GridRowOptions::default(),
9682 );
9683 for column_index in 0..3 {
9684 widgets::grid::grid_text_cell(
9685 ui,
9686 row,
9687 format!("containers.grid.row.{row_index}.cell.{column_index}"),
9688 format!("R{} C{}", row_index + 1, column_index + 1),
9689 widgets::grid::GridCellOptions {
9690 text_style: text(12.0, color(214, 224, 238)),
9691 ..Default::default()
9692 },
9693 );
9694 }
9695 }
9696
9697 widgets::sides(
9698 ui,
9699 body,
9700 "containers.sides",
9701 widgets::SidesOptions::default()
9702 .with_layout(LayoutStyle::row().with_width_percent(1.0).with_height(48.0))
9703 .with_gap(8.0)
9704 .with_visual(UiVisual::panel(
9705 color(20, 25, 32),
9706 Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
9707 4.0,
9708 )),
9709 |ui, left| {
9710 widgets::label(
9711 ui,
9712 left,
9713 "containers.sides.left.label",
9714 "Left side",
9715 text(12.0, color(220, 228, 238)),
9716 LayoutStyle::new().with_width_percent(1.0),
9717 );
9718 },
9719 |ui, right| {
9720 widgets::label(
9721 ui,
9722 right,
9723 "containers.sides.right.label",
9724 "Right side",
9725 text(12.0, color(220, 228, 238)),
9726 LayoutStyle::new().with_width_percent(1.0),
9727 );
9728 },
9729 );
9730
9731 widgets::columns(
9732 ui,
9733 body,
9734 "containers.columns",
9735 3,
9736 widgets::ColumnsOptions::default()
9737 .with_layout(LayoutStyle::row().with_width_percent(1.0).with_height(48.0))
9738 .with_gap(8.0),
9739 |ui, column, index| {
9740 widgets::label(
9741 ui,
9742 column,
9743 format!("containers.columns.{index}.label"),
9744 format!("Column {}", index + 1),
9745 text(12.0, color(220, 228, 238)),
9746 LayoutStyle::new().with_width_percent(1.0),
9747 );
9748 },
9749 );
9750
9751 let indented = widgets::indented_section(
9752 ui,
9753 body,
9754 "containers.indented",
9755 widgets::IndentOptions::default().with_amount(24.0),
9756 );
9757 widgets::label(
9758 ui,
9759 indented,
9760 "containers.indented.label",
9761 "Indented section",
9762 text(12.0, color(196, 210, 230)),
9763 LayoutStyle::new().with_width_percent(1.0),
9764 );
9765
9766 widgets::resize_container(
9767 ui,
9768 body,
9769 "containers.resize_container",
9770 widgets::ResizeContainerOptions::default().with_layout(
9771 LayoutStyle::column()
9772 .with_width_percent(1.0)
9773 .with_height(92.0)
9774 .with_flex_shrink(0.0),
9775 ),
9776 |ui, content| {
9777 widgets::label(
9778 ui,
9779 content,
9780 "containers.resize_container.label",
9781 "Resize container",
9782 text(12.0, color(220, 228, 238)),
9783 LayoutStyle::new().with_width_percent(1.0),
9784 );
9785 },
9786 );
9787
9788 widgets::scene(
9789 ui,
9790 body,
9791 "containers.scene",
9792 vec![
9793 ScenePrimitive::Rect(
9794 PaintRect::solid(UiRect::new(8.0, 12.0, 108.0, 46.0), color(48, 112, 184))
9795 .stroke(AlignedStroke::inside(StrokeStyle::new(
9796 color(132, 174, 222),
9797 1.0,
9798 )))
9799 .corner_radii(CornerRadii::uniform(6.0)),
9800 ),
9801 ScenePrimitive::Circle {
9802 center: UiPoint::new(150.0, 35.0),
9803 radius: 22.0,
9804 fill: color(111, 203, 159),
9805 stroke: Some(StrokeStyle::new(color(176, 236, 206), 1.0)),
9806 },
9807 ScenePrimitive::Line {
9808 from: UiPoint::new(188.0, 18.0),
9809 to: UiPoint::new(238.0, 52.0),
9810 stroke: StrokeStyle::new(color(232, 186, 88), 3.0),
9811 },
9812 ],
9813 widgets::SceneOptions::default()
9814 .with_layout(LayoutStyle::new().with_width(260.0).with_height(70.0))
9815 .accessibility_label("Scene primitives"),
9816 );
9817
9818 widgets::scroll_container(
9819 ui,
9820 body,
9821 "containers.scroll_area_with_bars",
9822 state.containers_scroll,
9823 widgets::ScrollContainerOptions::default()
9824 .with_axes(ScrollAxes::VERTICAL)
9825 .with_layout(LayoutStyle::column().with_width(260.0).with_height(116.0))
9826 .with_viewport_layout(
9827 LayoutStyle::column()
9828 .with_width(0.0)
9829 .with_height_percent(1.0)
9830 .with_flex_grow(1.0)
9831 .with_flex_shrink(1.0),
9832 ),
9833 |ui, viewport| {
9834 for index in 0..5 {
9835 widgets::label(
9836 ui,
9837 viewport,
9838 format!("containers.scroll_area_with_bars.row.{index}"),
9839 format!("Scrollable row {}", index + 1),
9840 text(12.0, color(200, 212, 228)),
9841 LayoutStyle::new()
9842 .with_width(232.0)
9843 .with_height(28.0)
9844 .with_flex_shrink(0.0),
9845 );
9846 }
9847 },
9848 );
9849
9850 widgets::label(
9851 ui,
9852 body,
9853 "containers.area.title",
9854 "Absolute area",
9855 text(12.0, color(166, 176, 190)),
9856 LayoutStyle::new().with_width_percent(1.0),
9857 );
9858 let area_host = ui.add_child(
9859 body,
9860 UiNode::container(
9861 "containers.area.host",
9862 LayoutStyle::new()
9863 .with_width_percent(1.0)
9864 .with_height(82.0)
9865 .with_flex_shrink(0.0),
9866 )
9867 .with_visual(UiVisual::panel(
9868 color(17, 20, 25),
9869 Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
9870 4.0,
9871 )),
9872 );
9873 widgets::container::area(
9874 ui,
9875 area_host,
9876 "containers.area",
9877 widgets::container::AreaOptions::new(UiRect::new(14.0, 14.0, 180.0, 44.0))
9878 .with_visual(UiVisual::panel(color(39, 72, 109), None, 4.0))
9879 .accessibility_label("Absolute positioned area"),
9880 |ui, area| {
9881 widgets::label(
9882 ui,
9883 area,
9884 "containers.area.label",
9885 "Area",
9886 text(12.0, color(238, 244, 252)),
9887 LayoutStyle::new().with_width_percent(1.0),
9888 );
9889 },
9890 );
9891}
9892
9893fn panel_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
9894 let body = section_with_min_viewport(ui, parent, "panels", "Panels", UiSize::new(520.0, 320.0));
9895 widgets::label(
9896 ui,
9897 body,
9898 "panels.title",
9899 "Drag the split bars to resize the docked panels.",
9900 text(12.0, color(166, 176, 190)),
9901 LayoutStyle::new().with_width_percent(1.0),
9902 );
9903 let shell = widgets::frame(
9904 ui,
9905 body,
9906 "panels.shell",
9907 widgets::FrameOptions::default().with_layout(
9908 LayoutStyle::column()
9909 .with_width_percent(1.0)
9910 .with_height(260.0)
9911 .with_flex_grow(1.0)
9912 .with_padding(0.0)
9913 .with_gap(0.0),
9914 ),
9915 );
9916 ext_widgets::split_pane(
9917 ui,
9918 shell,
9919 "panels.top_split",
9920 ext_widgets::SplitAxis::Vertical,
9921 state.panels_top_split,
9922 panel_split_options("panels.resize.top"),
9923 |ui, top| {
9924 panel_region(
9925 ui,
9926 top,
9927 "panels.top",
9928 widgets::PanelKind::Top,
9929 "Top",
9930 "Header controls",
9931 );
9932 },
9933 |ui, lower| {
9934 ext_widgets::split_pane(
9935 ui,
9936 lower,
9937 "panels.bottom_split",
9938 ext_widgets::SplitAxis::Vertical,
9939 state.panels_bottom_split,
9940 panel_split_options("panels.resize.bottom"),
9941 |ui, middle| {
9942 ext_widgets::split_pane(
9943 ui,
9944 middle,
9945 "panels.left_split",
9946 ext_widgets::SplitAxis::Horizontal,
9947 state.panels_left_split,
9948 panel_split_options("panels.resize.left"),
9949 |ui, left| {
9950 panel_region(
9951 ui,
9952 left,
9953 "panels.left",
9954 widgets::PanelKind::Left,
9955 "Left",
9956 "Navigation",
9957 );
9958 },
9959 |ui, center_and_right| {
9960 ext_widgets::split_pane(
9961 ui,
9962 center_and_right,
9963 "panels.right_split",
9964 ext_widgets::SplitAxis::Horizontal,
9965 state.panels_right_split,
9966 panel_split_options("panels.resize.right"),
9967 |ui, center| {
9968 panel_region(
9969 ui,
9970 center,
9971 "panels.center",
9972 widgets::PanelKind::Central,
9973 "Central",
9974 "Primary workspace",
9975 );
9976 },
9977 |ui, right| {
9978 panel_region(
9979 ui,
9980 right,
9981 "panels.right",
9982 widgets::PanelKind::Right,
9983 "Right",
9984 "Inspector",
9985 );
9986 },
9987 );
9988 },
9989 );
9990 },
9991 |ui, bottom| {
9992 panel_region(
9993 ui,
9994 bottom,
9995 "panels.bottom",
9996 widgets::PanelKind::Bottom,
9997 "Bottom",
9998 "Status and output",
9999 );
10000 },
10001 );
10002 },
10003 );
10004}
10005
10006fn panel_split_options(action: &'static str) -> ext_widgets::SplitPaneOptions {
10007 let mut options = ext_widgets::SplitPaneOptions::default().with_handle_action(action);
10008 options.handle_thickness = PANELS_SPLIT_HANDLE_THICKNESS;
10009 options.handle_visual = UiVisual::panel(color(58, 70, 88), None, 0.0);
10010 options.handle_hover_visual = Some(UiVisual::panel(color(100, 172, 244), None, 0.0));
10011 options
10012}
10013
10014fn panel_region(
10015 ui: &mut UiDocument,
10016 parent: UiNodeId,
10017 name: &'static str,
10018 kind: widgets::PanelKind,
10019 title: &'static str,
10020 detail: &'static str,
10021) -> UiNodeId {
10022 let panel = widgets::panel(
10023 ui,
10024 parent,
10025 name,
10026 widgets::PanelOptions {
10027 kind,
10028 layout: LayoutStyle::column()
10029 .with_width_percent(1.0)
10030 .with_height_percent(1.0)
10031 .with_padding(10.0)
10032 .with_gap(6.0),
10033 visual: UiVisual::panel(
10034 color(18, 23, 31),
10035 Some(StrokeStyle::new(color(66, 80, 98), 1.0)),
10036 0.0,
10037 ),
10038 accessibility_label: Some(title.to_string()),
10039 ..Default::default()
10040 },
10041 );
10042 widgets::label(
10043 ui,
10044 panel,
10045 format!("{name}.label"),
10046 title,
10047 text(13.0, color(232, 240, 250)),
10048 LayoutStyle::new().with_width_percent(1.0),
10049 );
10050 widgets::label(
10051 ui,
10052 panel,
10053 format!("{name}.detail"),
10054 detail,
10055 text(11.0, color(154, 166, 184)),
10056 LayoutStyle::new().with_width_percent(1.0),
10057 );
10058 panel
10059}
10060
10061fn form_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10062 let body = section_with_min_viewport(ui, parent, "forms", "Forms", UiSize::new(390.0, 0.0));
10063 let section = widgets::form_section(
10064 ui,
10065 body,
10066 "forms.profile",
10067 None::<String>,
10068 widgets::FormSectionOptions::default().with_layout(
10069 LayoutStyle::column()
10070 .with_width_percent(1.0)
10071 .with_padding(12.0)
10072 .with_gap(10.0),
10073 ),
10074 );
10075 profile_form_summary(ui, section.root, state);
10076
10077 let status_row = wrapping_row(ui, section.root, "forms.profile.status_flags", 6.0);
10078 form_status_chip(
10079 ui,
10080 status_row,
10081 "forms.profile.status.dirty",
10082 "dirty",
10083 state.form.dirty,
10084 );
10085 form_status_chip(
10086 ui,
10087 status_row,
10088 "forms.profile.status.pending",
10089 "pending",
10090 state.form.pending,
10091 );
10092 form_status_chip(
10093 ui,
10094 status_row,
10095 "forms.profile.status.submitted",
10096 "submitted",
10097 state.form.submitted,
10098 );
10099
10100 let mut name_options = widgets::FormRowOptions::default().required();
10101 if state.form_name_text.text().trim().is_empty() {
10102 name_options = name_options.invalid("Name is required");
10103 }
10104 let name = widgets::form_row(ui, section.root, "forms.profile.name", name_options);
10105 widgets::field_label(
10106 ui,
10107 name,
10108 "forms.profile.name.label",
10109 "Name",
10110 widgets::FieldLabelOptions::default().required(),
10111 );
10112 form_text_field(
10113 ui,
10114 name,
10115 "forms.profile.name.input",
10116 &state.form_name_text,
10117 FocusedTextInput::FormName,
10118 state,
10119 );
10120 if state.form_name_text.text().trim().is_empty() {
10121 widgets::field_validation_message(
10122 ui,
10123 name,
10124 "forms.profile.name.validation",
10125 ValidationMessage::error("Name is required"),
10126 widgets::ValidationMessageOptions::default(),
10127 );
10128 } else {
10129 widgets::field_help_text(
10130 ui,
10131 name,
10132 "forms.profile.name.help",
10133 "Shown in window titles and project lists.",
10134 widgets::FieldHelpOptions::default(),
10135 );
10136 }
10137
10138 let mut email_options = widgets::FormRowOptions::default().required();
10139 if !profile_email_valid(state.form_email_text.text()) {
10140 email_options = email_options.invalid("Use a complete email address");
10141 }
10142 let email = widgets::form_row(ui, section.root, "forms.profile.email", email_options);
10143 widgets::field_label(
10144 ui,
10145 email,
10146 "forms.profile.email.label",
10147 "Email",
10148 widgets::FieldLabelOptions::default().required(),
10149 );
10150 form_text_field(
10151 ui,
10152 email,
10153 "forms.profile.email.input",
10154 &state.form_email_text,
10155 FocusedTextInput::FormEmail,
10156 state,
10157 );
10158 if profile_email_valid(state.form_email_text.text()) {
10159 widgets::field_help_text(
10160 ui,
10161 email,
10162 "forms.profile.email.help",
10163 "Used for workspace invites and notifications.",
10164 widgets::FieldHelpOptions::default(),
10165 );
10166 } else {
10167 widgets::field_validation_message(
10168 ui,
10169 email,
10170 "forms.profile.email.validation",
10171 ValidationMessage::error("Use a complete email address"),
10172 widgets::ValidationMessageOptions::default(),
10173 );
10174 }
10175
10176 let role = widgets::form_row(
10177 ui,
10178 section.root,
10179 "forms.profile.role",
10180 widgets::FormRowOptions::default(),
10181 );
10182 widgets::field_label(
10183 ui,
10184 role,
10185 "forms.profile.role.label",
10186 "Role",
10187 widgets::FieldLabelOptions::default(),
10188 );
10189 form_text_field(
10190 ui,
10191 role,
10192 "forms.profile.role.input",
10193 &state.form_role_text,
10194 FocusedTextInput::FormRole,
10195 state,
10196 );
10197 widgets::field_validation_message(
10198 ui,
10199 role,
10200 "forms.profile.role.help",
10201 if state.form_role_text.text().trim().is_empty() {
10202 ValidationMessage::warning("Role can be added later")
10203 } else {
10204 ValidationMessage::info(
10205 "Form rows compose labels, controls, help, and validation text.",
10206 )
10207 },
10208 widgets::ValidationMessageOptions::default(),
10209 );
10210
10211 let newsletter = widgets::form_row(
10212 ui,
10213 section.root,
10214 "forms.profile.newsletter",
10215 widgets::FormRowOptions::default().with_accessibility_label("Newsletter preference"),
10216 );
10217 let mut newsletter_options =
10218 widgets::CheckboxOptions::default().with_action("forms.profile.newsletter.toggle");
10219 newsletter_options.layout = LayoutStyle::new().with_width_percent(1.0).with_height(30.0);
10220 newsletter_options.text_style = text(12.0, color(220, 228, 238));
10221 widgets::checkbox(
10222 ui,
10223 newsletter,
10224 "forms.profile.newsletter.input",
10225 "Send release notes",
10226 state.form_newsletter,
10227 newsletter_options,
10228 );
10229 widgets::field_help_text(
10230 ui,
10231 newsletter,
10232 "forms.profile.newsletter.help",
10233 "Checkboxes participate in the same form state as text fields.",
10234 widgets::FieldHelpOptions::default(),
10235 );
10236
10237 widgets::form_error_summary(
10238 ui,
10239 section.root,
10240 "forms.profile.errors",
10241 &state.form,
10242 widgets::FormErrorSummaryOptions::default(),
10243 );
10244 widgets::label(
10245 ui,
10246 section.root,
10247 "forms.profile.action_help",
10248 "Apply changes saves this draft and keeps editing. Submit profile saves and marks it submitted.",
10249 text(11.0, color(154, 166, 184)),
10250 LayoutStyle::new().with_width_percent(1.0),
10251 );
10252 let action_layout = Layout::row()
10253 .size(LayoutSize::new(
10254 LayoutDimension::percent(1.0),
10255 LayoutDimension::Auto,
10256 ))
10257 .gap(LayoutGap::points(8.0, 8.0))
10258 .flex_wrap(LayoutFlexWrap::Wrap)
10259 .to_layout_style();
10260 widgets::form_action_buttons(
10261 ui,
10262 section.root,
10263 "forms.profile.actions",
10264 &state.form,
10265 widgets::FormActionButtonsOptions::default()
10266 .with_layout(action_layout)
10267 .with_labels(widgets::FormActionLabels {
10268 submit: "Submit profile".to_string(),
10269 apply: "Apply changes".to_string(),
10270 cancel: "Cancel".to_string(),
10271 reset: "Reset".to_string(),
10272 })
10273 .include_reset(true)
10274 .with_action_prefix("forms.profile"),
10275 );
10276 widgets::label(
10277 ui,
10278 section.root,
10279 "forms.profile.status",
10280 format!("Status: {}", state.form_status),
10281 text(11.0, color(154, 166, 184)),
10282 LayoutStyle::new().with_width_percent(1.0),
10283 );
10284}
10285
10286fn overlay_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10287 let body =
10288 section_with_min_viewport(ui, parent, "overlays", "Overlays", UiSize::new(420.0, 0.0));
10289 let header = widgets::collapsing_header(
10290 ui,
10291 body,
10292 "overlays.collapsing",
10293 "Collapsing header",
10294 widgets::CollapsingHeaderOptions::default()
10295 .expanded(state.overlay_expanded)
10296 .with_toggle_action("overlays.collapsing.toggle"),
10297 );
10298 if let Some(panel) = header.body {
10299 widgets::label(
10300 ui,
10301 panel,
10302 "overlays.collapsing.body_text",
10303 "Expanded content lives under the header and remains part of normal layout.",
10304 text(12.0, color(196, 210, 230)),
10305 LayoutStyle::new().with_width_percent(1.0),
10306 );
10307 }
10308
10309 let controls = wrapping_row(ui, body, "overlays.controls", 8.0);
10310 let tooltip_visual = button_visual(58, 78, 96);
10311 let mut tooltip_options = widgets::ButtonOptions::new(LayoutStyle::new().with_height(32.0));
10312 tooltip_options.visual = tooltip_visual;
10313 tooltip_options.hovered_visual = Some(readable_button_hover_visual(tooltip_visual));
10314 tooltip_options.pressed_visual = Some(adjusted_button_visual(tooltip_visual, -62));
10315 tooltip_options.pressed_hovered_visual = Some(adjusted_button_visual(tooltip_visual, -24));
10316 tooltip_options.text_style = text(13.0, color(246, 249, 252));
10317 let tooltip_target = widgets::button(
10318 ui,
10319 controls,
10320 "overlays.tooltip_target",
10321 "Tooltip target",
10322 tooltip_options,
10323 );
10324 ui.node_mut(tooltip_target).set_tooltip(
10325 TooltipContent::new("Tooltip")
10326 .body("Tooltips render as overlay surfaces anchored to a target.")
10327 .shortcut_label("Ctrl+K")
10328 .disabled_reason("Disabled reasons can be announced without changing the trigger."),
10329 );
10330 ui.node_mut(tooltip_target)
10331 .set_tooltip_placement(TooltipPlacement::Below);
10332 ui.node_mut(tooltip_target)
10333 .set_tooltip_size(UiSize::new(240.0, 148.0));
10334 button(
10335 ui,
10336 controls,
10337 "overlays.popup.toggle",
10338 if state.overlay_popup_open {
10339 "Close popup"
10340 } else {
10341 "Open popup"
10342 },
10343 "overlays.popup.toggle",
10344 button_visual(48, 112, 184),
10345 );
10346 button(
10347 ui,
10348 controls,
10349 "overlays.modal.open",
10350 "Open modal",
10351 "overlays.modal.open",
10352 button_visual(58, 78, 96),
10353 );
10354
10355 widgets::label(
10356 ui,
10357 body,
10358 "overlays.tooltip_rect.label",
10359 "A right-edge target keeps its tooltip inside the preview.",
10360 text(12.0, color(166, 176, 190)),
10361 LayoutStyle::new().with_width_percent(1.0),
10362 );
10363 let preview_viewport = UiRect::new(0.0, 0.0, 420.0, 112.0);
10364 let tooltip_target = UiRect::new(328.0, 42.0, 64.0, 28.0);
10365 let tooltip_size = UiSize::new(176.0, 58.0);
10366 let placed_tooltip = widgets::tooltip::tooltip_rect(
10367 tooltip_target,
10368 tooltip_size,
10369 preview_viewport,
10370 TooltipPlacement::Right,
10371 8.0,
10372 None,
10373 );
10374 let clamped_preview = ui.add_child(
10375 body,
10376 UiNode::container(
10377 "overlays.tooltip_rect.preview",
10378 LayoutStyle::new()
10379 .with_width_percent(1.0)
10380 .with_height(112.0)
10381 .with_flex_shrink(0.0),
10382 )
10383 .with_visual(UiVisual::panel(
10384 color(12, 16, 22),
10385 Some(StrokeStyle::new(color(52, 64, 80), 1.0)),
10386 4.0,
10387 )),
10388 );
10389 ui.add_child(
10390 clamped_preview,
10391 UiNode::scene(
10392 "overlays.tooltip_rect.scene",
10393 vec![
10394 ScenePrimitive::Line {
10395 from: UiPoint::new(placed_tooltip.right() + 2.0, placed_tooltip.y + 29.0),
10396 to: UiPoint::new(tooltip_target.x - 2.0, tooltip_target.y + 14.0),
10397 stroke: StrokeStyle::new(color(92, 106, 128), 1.0),
10398 },
10399 ScenePrimitive::Rect(
10400 PaintRect::solid(placed_tooltip, color(24, 29, 38))
10401 .stroke(AlignedStroke::inside(StrokeStyle::new(
10402 color(92, 106, 128),
10403 1.0,
10404 )))
10405 .corner_radii(CornerRadii::uniform(4.0)),
10406 ),
10407 ScenePrimitive::Text(
10408 PaintText::new(
10409 "Tooltip",
10410 UiRect::new(
10411 placed_tooltip.x + 12.0,
10412 placed_tooltip.y + 9.0,
10413 placed_tooltip.width - 24.0,
10414 18.0,
10415 ),
10416 text(12.0, color(225, 233, 244)),
10417 )
10418 .multiline(false),
10419 ),
10420 ScenePrimitive::Text(
10421 PaintText::new(
10422 "Placed inside",
10423 UiRect::new(
10424 placed_tooltip.x + 12.0,
10425 placed_tooltip.y + 31.0,
10426 placed_tooltip.width - 24.0,
10427 18.0,
10428 ),
10429 text(10.0, color(156, 170, 190)),
10430 )
10431 .multiline(false),
10432 ),
10433 ScenePrimitive::Rect(
10434 PaintRect::solid(tooltip_target, color(48, 112, 184))
10435 .stroke(AlignedStroke::inside(StrokeStyle::new(
10436 color(132, 190, 255),
10437 1.0,
10438 )))
10439 .corner_radii(CornerRadii::uniform(3.0)),
10440 ),
10441 ScenePrimitive::Text(
10442 PaintText::new("Target", tooltip_target, text(10.0, color(240, 247, 255)))
10443 .horizontal_align(TextHorizontalAlign::Center)
10444 .vertical_align(TextVerticalAlign::Center)
10445 .multiline(false),
10446 ),
10447 ],
10448 LayoutStyle::new()
10449 .with_width_percent(1.0)
10450 .with_height_percent(1.0),
10451 ),
10452 );
10453
10454 widgets::label(
10455 ui,
10456 body,
10457 "overlays.popup.label",
10458 "Popup panel",
10459 text(12.0, color(166, 176, 190)),
10460 LayoutStyle::new().with_width_percent(1.0),
10461 );
10462 widgets::label(
10463 ui,
10464 body,
10465 "overlays.popup.status",
10466 if state.overlay_popup_open {
10467 "Popup overlay is open."
10468 } else {
10469 "Popup overlay is closed."
10470 },
10471 text(12.0, color(196, 210, 230)),
10472 LayoutStyle::new().with_width_percent(1.0),
10473 );
10474 let popup_host_layout = if state.overlay_popup_open {
10475 LayoutStyle::column()
10476 .with_width_percent(1.0)
10477 .with_height(128.0)
10478 .with_flex_shrink(0.0)
10479 } else {
10480 LayoutStyle::column()
10481 .with_width_percent(1.0)
10482 .with_padding(10.0)
10483 .with_flex_shrink(0.0)
10484 };
10485 let popup_host = ui.add_child(
10486 body,
10487 UiNode::container("overlays.popup.host", popup_host_layout).with_visual(UiVisual::panel(
10488 color(12, 16, 22),
10489 Some(StrokeStyle::new(color(52, 64, 80), 1.0)),
10490 4.0,
10491 )),
10492 );
10493 if state.overlay_popup_open {
10494 let popup = ext_widgets::popup_panel(
10495 ui,
10496 popup_host,
10497 "overlays.popup_panel",
10498 UiRect::new(10.0, 10.0, 220.0, 104.0),
10499 ext_widgets::PopupOptions {
10500 z_index: 4,
10501 portal: UiPortalTarget::Parent,
10502 accessibility: Some(
10503 AccessibilityMeta::new(AccessibilityRole::Dialog).label("Popup preview"),
10504 ),
10505 ..Default::default()
10506 },
10507 );
10508 let popup_body = ui.add_child(
10509 popup,
10510 UiNode::container(
10511 "overlays.popup_panel.body",
10512 LayoutStyle::column()
10513 .with_width_percent(1.0)
10514 .with_height_percent(1.0)
10515 .with_padding(10.0)
10516 .with_gap(6.0),
10517 ),
10518 );
10519 let popup_header = row(ui, popup_body, "overlays.popup_panel.header", 8.0);
10520 widgets::label(
10521 ui,
10522 popup_header,
10523 "overlays.popup_panel.label",
10524 "Popup panel",
10525 text(12.0, color(220, 228, 238)),
10526 LayoutStyle::new().with_width_percent(1.0),
10527 );
10528 let mut close = widgets::ButtonOptions::new(LayoutStyle::size(26.0, 22.0))
10529 .with_action("overlays.popup.close");
10530 close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
10531 close.hovered_visual = Some(button_visual(54, 70, 92));
10532 close.text_style = text(12.0, color(220, 228, 238));
10533 widgets::button(ui, popup_header, "overlays.popup_panel.close", "x", close);
10534 widgets::label(
10535 ui,
10536 popup_body,
10537 "overlays.popup_panel.body_text",
10538 "Popup content is conditionally rendered.",
10539 text(11.0, color(196, 210, 230)),
10540 LayoutStyle::new().with_width_percent(1.0),
10541 );
10542 } else {
10543 widgets::label(
10544 ui,
10545 popup_host,
10546 "overlays.popup.empty",
10547 "Open the popup to render an overlay inside this host.",
10548 text(12.0, color(154, 166, 184)),
10549 LayoutStyle::new().with_width_percent(1.0),
10550 );
10551 }
10552
10553 widgets::label(
10554 ui,
10555 body,
10556 "overlays.toasts.label",
10557 "Toasts",
10558 text(12.0, color(166, 176, 190)),
10559 LayoutStyle::new().with_width_percent(1.0),
10560 );
10561 let toast_controls = row(ui, body, "overlays.toasts.controls", 10.0);
10562 button(
10563 ui,
10564 toast_controls,
10565 "overlays.toasts.show",
10566 "Show toast",
10567 "toast.show",
10568 button_visual(48, 112, 184),
10569 );
10570 button(
10571 ui,
10572 toast_controls,
10573 "overlays.toasts.hide",
10574 "Hide",
10575 "toast.hide",
10576 button_visual(58, 78, 96),
10577 );
10578 widgets::label(
10579 ui,
10580 body,
10581 "overlays.toasts.status",
10582 if state.toast_visible {
10583 "Toast overlay is visible."
10584 } else {
10585 "Toast overlay is hidden."
10586 },
10587 text(12.0, color(196, 210, 230)),
10588 LayoutStyle::new().with_width_percent(1.0),
10589 );
10590 widgets::label(
10591 ui,
10592 body,
10593 "overlays.toasts.action_status",
10594 format!("Action: {}", state.toast_action_status),
10595 text(12.0, color(154, 166, 184)),
10596 LayoutStyle::new().with_width_percent(1.0),
10597 );
10598
10599 if state.overlay_modal_open {
10600 let modal = widgets::modal_dialog(
10601 ui,
10602 parent,
10603 "overlays.modal",
10604 "Modal dialog",
10605 widgets::ModalDialogOptions::default()
10606 .with_size(320.0, 180.0)
10607 .with_close_action("overlays.modal.close")
10608 .with_dismissal(ext_widgets::DialogDismissal::MODAL)
10609 .with_focus_restore(FocusRestoreTarget::Previous),
10610 );
10611 widgets::label(
10612 ui,
10613 modal.body,
10614 "overlays.modal.body.text",
10615 "Modal dialogs are portaled to the application overlay, include a scrim, and trap focus.",
10616 text(12.0, color(220, 228, 238)),
10617 LayoutStyle::new().with_width_percent(1.0),
10618 );
10619 button(
10620 ui,
10621 modal.body,
10622 "overlays.modal.body.close",
10623 "Close modal",
10624 "overlays.modal.close",
10625 button_visual(48, 112, 184),
10626 );
10627 }
10628}
10629
10630fn drag_drop_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10631 let body = section_with_min_viewport(
10632 ui,
10633 parent,
10634 "drag_drop",
10635 "Drag and drop",
10636 UiSize::new(420.0, 0.0),
10637 );
10638 widgets::label(
10639 ui,
10640 body,
10641 "drag_drop.sources.label",
10642 "Drag sources",
10643 text(12.0, color(166, 176, 190)),
10644 LayoutStyle::new().with_width_percent(1.0),
10645 );
10646 let sources = wrapping_row(ui, body, "drag_drop.sources", 8.0);
10647 widgets::dnd_drag_source(
10648 ui,
10649 sources,
10650 "drag_drop.text_source",
10651 "Text payload",
10652 DragPayload::text("Operad payload"),
10653 widgets::DragSourceOptions::default()
10654 .with_layout(drag_source_layout())
10655 .with_kind(DragDropSurfaceKind::ListRow)
10656 .with_allowed_operations([DragOperation::Copy, DragOperation::Move])
10657 .with_action("drag_drop.text_source")
10658 .with_accessibility_hint("Start a text drag operation"),
10659 );
10660 widgets::dnd_drag_source(
10661 ui,
10662 sources,
10663 "drag_drop.file_source",
10664 "File payload",
10665 DragPayload::files(["/tmp/showcase.scene"]),
10666 widgets::DragSourceOptions::default()
10667 .with_layout(drag_source_layout())
10668 .with_kind(DragDropSurfaceKind::Asset)
10669 .with_drag_image_policy(widgets::DragImagePolicy::image_key(
10670 BuiltInIcon::Folder.key(),
10671 UiSize::new(120.0, 36.0),
10672 UiPoint::new(10.0, 10.0),
10673 ))
10674 .with_allowed_operations([DragOperation::Copy])
10675 .with_action("drag_drop.file_source"),
10676 );
10677 widgets::dnd_drag_source(
10678 ui,
10679 sources,
10680 "drag_drop.bytes_source",
10681 "Image bytes",
10682 DragPayload::bytes(DragBytes::new("image/png", vec![137, 80, 78, 71]).name("sprite.png")),
10683 widgets::DragSourceOptions::default()
10684 .with_layout(drag_source_layout())
10685 .with_kind(DragDropSurfaceKind::Asset)
10686 .with_action("drag_drop.bytes_source")
10687 .without_drag_image(),
10688 );
10689
10690 widgets::label(
10691 ui,
10692 body,
10693 "drag_drop.zones.label",
10694 "Drop zones",
10695 text(12.0, color(166, 176, 190)),
10696 LayoutStyle::new().with_width_percent(1.0),
10697 );
10698 let zones = wrapping_row(ui, body, "drag_drop.zones", 8.0);
10699 let accepted_options = widgets::DropZoneOptions::default()
10700 .with_layout(drop_zone_layout())
10701 .with_kind(DragDropSurfaceKind::EditorSurface)
10702 .with_accepted_payload(DropPayloadFilter::empty().text())
10703 .with_accepted_operations([DragOperation::Copy, DragOperation::Move])
10704 .with_action("drag_drop.accept_text")
10705 .with_accessibility_hint("Accepts text payloads");
10706 let accepted = widgets::dnd_drop_zone(
10707 ui,
10708 zones,
10709 "drag_drop.accept_text",
10710 "Text accepted",
10711 accepted_options.clone(),
10712 );
10713 widgets::drag_drop::dnd_apply_drop_zone_preview(
10714 ui,
10715 accepted.root,
10716 &accepted_options,
10717 widgets::drag_drop::DropZonePreviewState::Accepted,
10718 );
10719
10720 let rejected_options = widgets::DropZoneOptions::default()
10721 .with_layout(drop_zone_layout())
10722 .with_kind(DragDropSurfaceKind::Asset)
10723 .with_accepted_payload(DropPayloadFilter::empty().files())
10724 .with_action("drag_drop.files_only");
10725 let rejected = widgets::dnd_drop_zone(
10726 ui,
10727 zones,
10728 "drag_drop.files_only",
10729 "Files only",
10730 rejected_options.clone(),
10731 );
10732 widgets::drag_drop::dnd_apply_drop_zone_preview(
10733 ui,
10734 rejected.root,
10735 &rejected_options,
10736 widgets::drag_drop::DropZonePreviewState::Rejected,
10737 );
10738 let image_options = widgets::DropZoneOptions::default()
10739 .with_layout(drop_zone_layout())
10740 .with_kind(DragDropSurfaceKind::Asset)
10741 .with_accepted_payload(DropPayloadFilter::empty().mime_type("image/*"))
10742 .with_accepted_operations([DragOperation::Copy])
10743 .with_action("drag_drop.image_bytes");
10744 let image_zone = widgets::dnd_drop_zone(
10745 ui,
10746 zones,
10747 "drag_drop.image_bytes",
10748 "Image bytes",
10749 image_options.clone(),
10750 );
10751 widgets::drag_drop::dnd_apply_drop_zone_preview(
10752 ui,
10753 image_zone.root,
10754 &image_options,
10755 widgets::drag_drop::DropZonePreviewState::Hovered,
10756 );
10757
10758 let disabled_options = widgets::DropZoneOptions::default()
10759 .with_layout(drop_zone_layout())
10760 .with_kind(DragDropSurfaceKind::EditorSurface)
10761 .with_accepted_payload(DropPayloadFilter::any())
10762 .with_action("drag_drop.disabled")
10763 .disabled();
10764 let disabled_zone = widgets::dnd_drop_zone(
10765 ui,
10766 zones,
10767 "drag_drop.disabled",
10768 "Disabled",
10769 disabled_options.clone(),
10770 );
10771 widgets::drag_drop::dnd_apply_drop_zone_preview(
10772 ui,
10773 disabled_zone.root,
10774 &disabled_options,
10775 widgets::drag_drop::DropZonePreviewState::Disabled,
10776 );
10777
10778 let operation_row = wrapping_row(ui, body, "drag_drop.operations", 6.0);
10779 dnd_operation_chip(ui, operation_row, "drag_drop.operation.copy", "copy");
10780 dnd_operation_chip(ui, operation_row, "drag_drop.operation.move", "move");
10781 dnd_operation_chip(ui, operation_row, "drag_drop.operation.link", "link");
10782 widgets::label(
10783 ui,
10784 body,
10785 "drag_drop.status",
10786 format!("Status: {}", state.drag_drop_status),
10787 text(11.0, color(154, 166, 184)),
10788 LayoutStyle::new().with_width_percent(1.0),
10789 );
10790}
10791
10792fn media_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10793 let body = section_with_min_viewport(
10794 ui,
10795 parent,
10796 "media",
10797 "Media",
10798 UiSize::new(MEDIA_ICON_TILE_WIDTH, 0.0),
10799 );
10800 widgets::label(
10801 ui,
10802 body,
10803 "media.icons.label",
10804 "Built-in icons",
10805 text(12.0, color(166, 176, 190)),
10806 LayoutStyle::new().with_width_percent(1.0),
10807 );
10808 let icon_columns = media_icon_columns(state);
10809 let icons = media_icon_grid(
10810 ui,
10811 body,
10812 "media.icons",
10813 icon_columns,
10814 BuiltInIcon::COMMON.len(),
10815 );
10816 for icon in BuiltInIcon::COMMON {
10817 media_icon_tile(ui, icons, icon);
10818 }
10819
10820 widgets::label(
10821 ui,
10822 body,
10823 "media.variants.label",
10824 "Image variants",
10825 text(12.0, color(166, 176, 190)),
10826 LayoutStyle::new().with_width_percent(1.0),
10827 );
10828 let variants = wrapping_row(ui, body, "media.variants", 10.0);
10829 widgets::image(
10830 ui,
10831 variants,
10832 "media.image.user_png",
10833 ImageContent::from(ImageHandle::app(SHOWCASE_USER_IMAGE_KEY)),
10834 widgets::ImageOptions::default()
10835 .with_layout(media_preview_image_layout())
10836 .with_accessibility_label("User supplied PNG image"),
10837 );
10838 widgets::image(
10839 ui,
10840 variants,
10841 "media.image.untinted",
10842 icon_image(BuiltInIcon::Play),
10843 widgets::ImageOptions::default()
10844 .with_layout(media_preview_image_layout())
10845 .with_accessibility_label("Untinted play icon"),
10846 );
10847 widgets::image(
10848 ui,
10849 variants,
10850 "media.image.warning",
10851 ImageContent::new(BuiltInIcon::Warning.key()).tinted(color(232, 186, 88)),
10852 widgets::ImageOptions::default()
10853 .with_layout(media_preview_image_layout())
10854 .with_accessibility_label("Tinted warning icon"),
10855 );
10856 widgets::image(
10857 ui,
10858 variants,
10859 "media.image.shader",
10860 ImageContent::new(BuiltInIcon::Grid.key()).tinted(color(118, 183, 255)),
10861 widgets::ImageOptions::default()
10862 .with_layout(media_preview_image_layout())
10863 .with_shader(ShaderEffect::tint(color(169, 119, 255), 0.65))
10864 .with_accessibility_label("Shader-decorated grid icon"),
10865 );
10866}
10867
10868fn shader_effect_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
10869 let body = section_with_min_viewport(
10870 ui,
10871 parent,
10872 "shaders",
10873 "Shader effects",
10874 UiSize::new(420.0, 280.0),
10875 );
10876 widgets::label(
10877 ui,
10878 body,
10879 "shaders.effects.label",
10880 "Built-in effects",
10881 text(12.0, color(166, 176, 190)),
10882 LayoutStyle::new().with_width_percent(1.0),
10883 );
10884 let phase = (state.progress_phase / std::f32::consts::TAU).fract();
10885 let previews = wrapping_row(ui, body, "shaders.effects", 10.0);
10886 shader_effect_preview_card(ui, previews, "base", "Base", None);
10887 shader_effect_preview_card(
10888 ui,
10889 previews,
10890 "tint",
10891 "Tint",
10892 Some(ShaderEffect::tint(color(252, 186, 90), 0.72)),
10893 );
10894 shader_effect_preview_card(
10895 ui,
10896 previews,
10897 "shine",
10898 "Shine",
10899 Some(ShaderEffect::shine(phase, 0.55).uniform("width", 0.14)),
10900 );
10901 shader_effect_preview_card(
10902 ui,
10903 previews,
10904 "glow",
10905 "Glow",
10906 Some(ShaderEffect::glow(color(118, 183, 255), 0.9, 7.0)),
10907 );
10908
10909 widgets::label(
10910 ui,
10911 body,
10912 "shaders.widgets.label",
10913 "Applied to widgets",
10914 text(12.0, color(166, 176, 190)),
10915 LayoutStyle::new().with_width_percent(1.0),
10916 );
10917 let panel = ui.add_child(
10918 body,
10919 UiNode::container(
10920 "shaders.widgets",
10921 LayoutStyle::column()
10922 .with_width_percent(1.0)
10923 .with_padding(10.0)
10924 .with_gap(10.0)
10925 .with_flex_shrink(0.0),
10926 )
10927 .with_visual(UiVisual::panel(
10928 color(13, 18, 25),
10929 Some(StrokeStyle::new(color(48, 61, 78), 1.0)),
10930 4.0,
10931 )),
10932 );
10933 let control_row = wrapping_row(ui, panel, "shaders.widgets.controls", 10.0);
10934 let mut shine_button = widgets::ButtonOptions::new(
10935 LayoutStyle::new()
10936 .with_width(150.0)
10937 .with_height(34.0)
10938 .with_flex_shrink(0.0),
10939 );
10940 shine_button.leading_image = Some(icon_image(BuiltInIcon::Settings));
10941 shine_button.image_shader = Some(ShaderEffect::tint(color(111, 203, 159), 0.85));
10942 shine_button.shader = Some(ShaderEffect::shine(phase, 0.45).uniform("width", 0.12));
10943 widgets::button(
10944 ui,
10945 control_row,
10946 "shaders.widgets.button",
10947 "Shine button",
10948 shine_button,
10949 );
10950
10951 widgets::checkbox_with_state(
10952 ui,
10953 control_row,
10954 "shaders.widgets.checkbox",
10955 "Glow check",
10956 widgets::CheckboxState::Checked,
10957 widgets::CheckboxOptions::default()
10958 .with_check_color(color(118, 183, 255))
10959 .with_check_shader(ShaderEffect::glow(color(118, 183, 255), 1.0, 4.0)),
10960 );
10961
10962 let progress_value = smooth_loop(state.progress_phase * 0.8, 0.2) * 100.0;
10963 let mut progress = ext_widgets::ProgressIndicatorOptions::default();
10964 progress.layout = LayoutStyle::new().with_width_percent(1.0).with_height(12.0);
10965 progress.fill_visual = UiVisual::panel(color(111, 203, 159), None, 4.0);
10966 progress.fill_shader = Some(ShaderEffect::shine(phase, 0.5).uniform("width", 0.16));
10967 progress.accessibility_label = Some("Shadered progress fill".to_string());
10968 ext_widgets::progress_indicator(
10969 ui,
10970 panel,
10971 "shaders.widgets.progress",
10972 ext_widgets::ProgressIndicatorValue::percent(progress_value),
10973 progress,
10974 );
10975
10976 let mut slider_options = widgets::SliderOptions::default();
10977 slider_options.layout = LayoutStyle::new()
10978 .with_width_percent(1.0)
10979 .with_height(28.0)
10980 .with_flex_shrink(0.0);
10981 slider_options.fill_shader = Some(ShaderEffect::tint(color(169, 119, 255), 0.55));
10982 slider_options.thumb_shader = Some(ShaderEffect::glow(color(252, 186, 90), 0.9, 4.0));
10983 slider_options.accessibility_label = Some("Shadered slider".to_string());
10984 widgets::slider(
10985 ui,
10986 panel,
10987 "shaders.widgets.slider",
10988 smooth_loop(state.progress_phase * 0.6, 0.4),
10989 0.0..1.0,
10990 slider_options,
10991 );
10992}
10993
10994fn shader_effect_preview_card(
10995 ui: &mut UiDocument,
10996 parent: UiNodeId,
10997 name: &'static str,
10998 label: &'static str,
10999 shader: Option<ShaderEffect>,
11000) {
11001 let tile = ui.add_child(
11002 parent,
11003 UiNode::container(
11004 format!("shaders.effect_tile.{name}"),
11005 LayoutStyle::column()
11006 .with_width(96.0)
11007 .with_height(104.0)
11008 .with_padding(8.0)
11009 .with_gap(8.0)
11010 .with_flex_shrink(0.0),
11011 )
11012 .with_visual(UiVisual::panel(
11013 color(17, 22, 30),
11014 Some(StrokeStyle::new(color(50, 62, 78), 1.0)),
11015 4.0,
11016 ))
11017 .with_accessibility(AccessibilityMeta::new(AccessibilityRole::Group).label(label)),
11018 );
11019 let mut swatch = UiNode::container(
11020 format!("shaders.effect.{name}.swatch"),
11021 LayoutStyle::new()
11022 .with_width_percent(1.0)
11023 .with_height(50.0)
11024 .with_flex_shrink(0.0),
11025 )
11026 .with_visual(UiVisual::panel(
11027 color(64, 109, 194),
11028 Some(StrokeStyle::new(color(138, 164, 194), 1.0)),
11029 8.0,
11030 ));
11031 if let Some(shader) = shader {
11032 swatch = swatch.with_shader(shader);
11033 }
11034 ui.add_child(tile, swatch);
11035 widgets::label(
11036 ui,
11037 tile,
11038 format!("shaders.effect.{name}.label"),
11039 label,
11040 text(11.0, color(204, 216, 232)),
11041 LayoutStyle::new().with_width_percent(1.0),
11042 );
11043}
11044
11045fn shader_lab_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
11046 let body = section_with_min_viewport(
11047 ui,
11048 parent,
11049 "shader_lab",
11050 "Shader lab",
11051 UiSize::new(SHADER_LAB_CONTENT_MIN_WIDTH, SHADER_LAB_CONTENT_MIN_HEIGHT),
11052 );
11053 let source_error = state.shader_lab_source_error.as_deref();
11054 let source_valid = source_error.is_none();
11055
11056 let mut split_options = ext_widgets::SplitPaneOptions::default()
11057 .with_handle_action("shader_lab.workspace.resize")
11058 .with_handle_hover_visual(UiVisual::panel(color(96, 166, 238), None, 2.0));
11059 split_options.layout = Layout::row()
11060 .size(LayoutSize::new(
11061 LayoutDimension::percent(1.0),
11062 LayoutDimension::points(SHADER_LAB_WORKSPACE_HEIGHT),
11063 ))
11064 .min_size(LayoutSize::points(
11065 SHADER_LAB_CONTENT_MIN_WIDTH,
11066 SHADER_LAB_WORKSPACE_HEIGHT,
11067 ))
11068 .flex(0.0, 0.0, LayoutDimension::Auto)
11069 .to_layout_style();
11070 split_options.handle_thickness = SHADER_LAB_SPLIT_HANDLE_THICKNESS;
11071 split_options.handle_visual = UiVisual::panel(color(48, 61, 78), None, 2.0);
11072
11073 ext_widgets::split_pane(
11074 ui,
11075 body,
11076 "shader_lab.workspace",
11077 ext_widgets::SplitAxis::Horizontal,
11078 state.shader_lab_split,
11079 split_options,
11080 |ui, preview_pane| {
11081 shader_lab_preview_column(ui, preview_pane, state, source_error, source_valid);
11082 },
11083 |ui, editor_pane| {
11084 shader_lab_editor_column(ui, editor_pane, state, source_error);
11085 },
11086 );
11087}
11088
11089fn shader_lab_preview_column(
11090 ui: &mut UiDocument,
11091 parent: UiNodeId,
11092 state: &ShowcaseState,
11093 source_error: Option<&str>,
11094 source_valid: bool,
11095) {
11096 let preview_column = ui.add_child(
11097 parent,
11098 UiNode::container(
11099 "shader_lab.preview.column",
11100 LayoutStyle::column()
11101 .with_width_percent(1.0)
11102 .with_height_percent(1.0)
11103 .with_gap(8.0)
11104 .with_flex_shrink(1.0),
11105 ),
11106 );
11107 let target_row = row(ui, preview_column, "shader_lab.target.row", 8.0);
11108 widgets::label(
11109 ui,
11110 target_row,
11111 "shader_lab.target.caption",
11112 "Preview",
11113 text(12.0, color(166, 176, 190)),
11114 LayoutStyle::new()
11115 .with_width(58.0)
11116 .with_height(30.0)
11117 .with_flex_shrink(0.0),
11118 );
11119 shader_lab_dropdown_select(
11120 ui,
11121 target_row,
11122 "shader_lab.target",
11123 &shader_lab_target_options(),
11124 &state.shader_lab_target_menu,
11125 160.0,
11126 "Preview target",
11127 "Shader lab preview target",
11128 );
11129 shader_lab_preview_controls(ui, preview_column, state);
11130
11131 let preview = ui.add_child(
11132 preview_column,
11133 UiNode::container(
11134 "shader_lab.preview.surface",
11135 LayoutStyle::column()
11136 .with_width_percent(1.0)
11137 .with_height(0.0)
11138 .with_flex_grow(1.0)
11139 .with_padding(18.0)
11140 .with_gap(8.0)
11141 .with_align_items(taffy::prelude::AlignItems::Center)
11142 .with_justify_content(taffy::prelude::JustifyContent::Center)
11143 .with_flex_shrink(0.0),
11144 )
11145 .with_visual(UiVisual::panel(
11146 color(8, 12, 18),
11147 Some(StrokeStyle::new(color(48, 61, 78), 1.0)),
11148 4.0,
11149 )),
11150 );
11151 shader_lab_preview(ui, preview, state, source_valid);
11152
11153 if let Some(status) = shader_lab_status_label(state, source_error) {
11154 widgets::label(
11155 ui,
11156 preview_column,
11157 "shader_lab.preview.status",
11158 status,
11159 text(11.0, color(166, 176, 190)),
11160 LayoutStyle::new().with_width_percent(1.0),
11161 );
11162 }
11163 shader_lab_material_contract_demo(ui, preview_column, state);
11164}
11165
11166fn shader_lab_preview_controls(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
11167 let controls = ui.add_child(
11168 parent,
11169 UiNode::container(
11170 "shader_lab.preview.controls",
11171 LayoutStyle::column()
11172 .with_width_percent(1.0)
11173 .with_gap(6.0)
11174 .with_flex_shrink(0.0),
11175 ),
11176 );
11177 let text_row = wrapping_row(ui, controls, "shader_lab.preview.text_controls", 8.0);
11178 shader_lab_option_checkbox(
11179 ui,
11180 text_row,
11181 "shader_lab.frame_text.toggle",
11182 "Frame",
11183 state.shader_lab_show_frame_text,
11184 );
11185 shader_lab_option_checkbox(
11186 ui,
11187 text_row,
11188 "shader_lab.button_text.toggle",
11189 "Button",
11190 state.shader_lab_show_button_text,
11191 );
11192
11193 let style_row = wrapping_row(ui, controls, "shader_lab.preview.style_controls", 8.0);
11194 shader_lab_slider_control(
11195 ui,
11196 style_row,
11197 "shader_lab.surface.stroke",
11198 "Border",
11199 state.shader_lab_surface_stroke_width,
11200 SHADER_LAB_SURFACE_STROKE_MAX,
11201 1,
11202 );
11203 shader_lab_slider_control(
11204 ui,
11205 style_row,
11206 "shader_lab.surface.radius",
11207 "Radius",
11208 state.shader_lab_surface_radius,
11209 SHADER_LAB_SURFACE_RADIUS_MAX,
11210 0,
11211 );
11212}
11213
11214fn shader_lab_material_contract_demo(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
11215 let panel = ui.add_child(
11216 parent,
11217 UiNode::container(
11218 "shader_lab.material",
11219 LayoutStyle::column()
11220 .with_width_percent(1.0)
11221 .with_gap(6.0)
11222 .with_flex_shrink(0.0),
11223 ),
11224 );
11225 widgets::label(
11226 ui,
11227 panel,
11228 "shader_lab.material.title",
11229 "Material",
11230 text(12.0, color(186, 198, 216)),
11231 LayoutStyle::new().with_width_percent(1.0),
11232 );
11233 let controls = wrapping_row(ui, panel, "shader_lab.material.controls", 8.0);
11234 shader_lab_labeled_dropdown(
11235 ui,
11236 controls,
11237 "shader_lab.material.shader",
11238 "Shader",
11239 &shader_lab_material_shader_options(),
11240 &state.shader_lab_material_shader_menu,
11241 132.0,
11242 );
11243 shader_lab_labeled_dropdown(
11244 ui,
11245 controls,
11246 "shader_lab.material.shape",
11247 "Shape",
11248 &shader_lab_material_shape_options(),
11249 &state.shader_lab_material_shape_menu,
11250 132.0,
11251 );
11252 shader_lab_labeled_dropdown(
11253 ui,
11254 controls,
11255 "shader_lab.material.geometry",
11256 "Geometry",
11257 &shader_lab_material_geometry_options(),
11258 &state.shader_lab_material_geometry_menu,
11259 140.0,
11260 );
11261 shader_lab_slider_control(
11262 ui,
11263 controls,
11264 "shader_lab.material.outset",
11265 "Outset",
11266 state.shader_lab_material_outset,
11267 SHADER_LAB_MATERIAL_OUTSET_MAX,
11268 0,
11269 );
11270
11271 let contracts_layout = Layout::column()
11272 .size(LayoutSize::new(
11273 LayoutDimension::percent(1.0),
11274 LayoutDimension::Auto,
11275 ))
11276 .padding(LayoutSpacing::new(
11277 LayoutLength::points(4.0),
11278 LayoutLength::points(4.0),
11279 LayoutLength::points(SHADER_LAB_MATERIAL_OUTSET_MAX),
11280 LayoutLength::points(8.0),
11281 ))
11282 .flex(0.0, 0.0, LayoutDimension::Auto)
11283 .to_layout_style();
11284 let contracts_shell = ui.add_child(
11285 panel,
11286 UiNode::container("shader_lab.material.contracts.shell", contracts_layout),
11287 );
11288 let row = wrapping_row(ui, contracts_shell, "shader_lab.material.contracts", 8.0);
11289 shader_lab_material_chip(
11290 ui,
11291 row,
11292 "shader_lab.material.current",
11293 "Selected",
11294 shader_lab_selected_material(state),
11295 shader_lab_material_visual(state.shader_lab_material_shape),
11296 );
11297 shader_lab_material_chip(
11298 ui,
11299 row,
11300 "shader_lab.material.outset",
11301 "Glow",
11302 ElementMaterial::shader(ShaderEffect::glow(
11303 color(100, 180, 255),
11304 0.95,
11305 SHADER_LAB_MATERIAL_OUTSET,
11306 ))
11307 .with_paint_outset(LayoutInsets::points(
11308 state
11309 .shader_lab_material_outset
11310 .clamp(0.0, SHADER_LAB_MATERIAL_OUTSET_MAX),
11311 )),
11312 UiVisual::panel(color(32, 64, 96), None, 8.0),
11313 );
11314 shader_lab_material_chip(
11315 ui,
11316 row,
11317 "shader_lab.material.circle_hit",
11318 "Circle",
11319 ElementMaterial::new()
11320 .with_clip_shape(ElementShape::circle())
11321 .with_hit_shape(ElementShape::circle()),
11322 UiVisual::panel(
11323 color(74, 133, 198),
11324 Some(StrokeStyle::new(color(212, 232, 255), 1.0)),
11325 999.0,
11326 ),
11327 );
11328 shader_lab_material_chip(
11329 ui,
11330 row,
11331 "shader_lab.material.geometry_chip",
11332 "Warp",
11333 ElementMaterial::new()
11334 .with_paint_outset(LayoutInsets::points(8.0))
11335 .with_geometry_effect(GeometryEffect::wave(8.0)),
11336 UiVisual::panel(
11337 color(101, 70, 170),
11338 Some(StrokeStyle::new(color(214, 196, 255), 1.0)),
11339 6.0,
11340 ),
11341 );
11342}
11343
11344fn shader_lab_labeled_dropdown(
11345 ui: &mut UiDocument,
11346 parent: UiNodeId,
11347 name: &'static str,
11348 label: &'static str,
11349 options: &[ext_widgets::SelectOption],
11350 state: &ext_widgets::SelectMenuState,
11351 width: f32,
11352) {
11353 let control = ui.add_child(
11354 parent,
11355 UiNode::container(
11356 format!("{name}.control"),
11357 LayoutStyle::row()
11358 .with_width(width + 74.0)
11359 .with_height(30.0)
11360 .with_gap(6.0)
11361 .with_align_items(taffy::prelude::AlignItems::Center)
11362 .with_flex_shrink(0.0),
11363 ),
11364 );
11365 widgets::label(
11366 ui,
11367 control,
11368 format!("{name}.caption"),
11369 label,
11370 text(12.0, color(166, 176, 190)),
11371 LayoutStyle::new()
11372 .with_width(66.0)
11373 .with_height(30.0)
11374 .with_flex_shrink(0.0),
11375 );
11376 shader_lab_dropdown_select(ui, control, name, options, state, width, label, label);
11377}
11378
11379fn shader_lab_dropdown_select(
11380 ui: &mut UiDocument,
11381 parent: UiNodeId,
11382 name: &'static str,
11383 options: &[ext_widgets::SelectOption],
11384 state: &ext_widgets::SelectMenuState,
11385 width: f32,
11386 placeholder: &'static str,
11387 accessibility_label: &'static str,
11388) {
11389 let anchor = ui.add_child(
11390 parent,
11391 UiNode::container(
11392 format!("{name}.anchor"),
11393 LayoutStyle::new()
11394 .with_width(width)
11395 .with_height(30.0)
11396 .with_flex_shrink(0.0),
11397 ),
11398 );
11399 let nodes = ext_widgets::dropdown_select(
11400 ui,
11401 anchor,
11402 name,
11403 options,
11404 state,
11405 Some(select_popup(
11406 UiRect::new(0.0, 0.0, width, 30.0),
11407 UiRect::new(0.0, 0.0, width + 48.0, 240.0),
11408 )),
11409 dropdown_select_options(width, name, placeholder, accessibility_label),
11410 );
11411 ui.node_mut(nodes.trigger)
11412 .set_action(format!("{name}.toggle"));
11413}
11414
11415fn shader_lab_selected_material(state: &ShowcaseState) -> ElementMaterial {
11416 let shape = state.shader_lab_material_shape.shape();
11417 let mut material = ElementMaterial::new()
11418 .with_paint_outset(LayoutInsets::points(
11419 state
11420 .shader_lab_material_outset
11421 .clamp(0.0, SHADER_LAB_MATERIAL_OUTSET_MAX),
11422 ))
11423 .with_clip_shape(shape.clone())
11424 .with_hit_shape(shape)
11425 .with_geometry_effect(state.shader_lab_material_geometry.effect());
11426 if let Some(shader) = shader_lab_material_shader_effect(state) {
11427 material = material.with_shader(shader);
11428 }
11429 material
11430}
11431
11432fn shader_lab_material_shader_effect(state: &ShowcaseState) -> Option<ShaderEffect> {
11433 let phase = state.progress_phase.rem_euclid(1.0);
11434 match state.shader_lab_material_shader {
11435 ShaderLabMaterialShader::None => None,
11436 ShaderLabMaterialShader::Tint => Some(ShaderEffect::tint(color(255, 196, 92), 0.62)),
11437 ShaderLabMaterialShader::Shine => Some(ShaderEffect::shine(phase, 0.92)),
11438 ShaderLabMaterialShader::Glow => Some(ShaderEffect::glow(
11439 color(100, 180, 255),
11440 0.95,
11441 state
11442 .shader_lab_material_outset
11443 .clamp(0.0, SHADER_LAB_MATERIAL_OUTSET_MAX),
11444 )),
11445 ShaderLabMaterialShader::Plasma => {
11446 Some(ShaderEffect::plasma(phase, color(82, 190, 255), 0.75, 12.0))
11447 }
11448 ShaderLabMaterialShader::Rings => {
11449 Some(ShaderEffect::rings(phase, color(232, 170, 88), 0.78, 11.0))
11450 }
11451 ShaderLabMaterialShader::Grid => {
11452 Some(ShaderEffect::grid(phase, color(156, 132, 255), 0.85, 9.0))
11453 }
11454 }
11455}
11456
11457fn shader_lab_material_visual(shape: ShaderLabMaterialShape) -> UiVisual {
11458 UiVisual::panel(
11459 color(39, 71, 114),
11460 Some(StrokeStyle::new(color(168, 205, 255), 1.0)),
11461 shape.visual_radius(),
11462 )
11463}
11464
11465fn shader_lab_material_chip(
11466 ui: &mut UiDocument,
11467 parent: UiNodeId,
11468 name: &'static str,
11469 label: &'static str,
11470 material: ElementMaterial,
11471 visual: UiVisual,
11472) {
11473 let chip = ui.add_child(
11474 parent,
11475 UiNode::container(
11476 name,
11477 LayoutStyle::row()
11478 .with_width(156.0)
11479 .with_height(44.0)
11480 .with_padding(8.0)
11481 .with_align_items(taffy::prelude::AlignItems::Center)
11482 .with_justify_content(taffy::prelude::JustifyContent::Center)
11483 .with_flex_shrink(0.0),
11484 )
11485 .with_visual(visual)
11486 .with_material(material)
11487 .with_accessibility(
11488 AccessibilityMeta::new(AccessibilityRole::Image).label(format!("{label} material")),
11489 ),
11490 );
11491 widgets::label(
11492 ui,
11493 chip,
11494 format!("{name}.text"),
11495 label,
11496 text(11.0, color(246, 249, 252)),
11497 LayoutStyle::new().with_flex_shrink(0.0),
11498 );
11499}
11500
11501fn shader_lab_option_checkbox(
11502 ui: &mut UiDocument,
11503 parent: UiNodeId,
11504 name: &'static str,
11505 label: &'static str,
11506 checked: bool,
11507) {
11508 let mut options = widgets::CheckboxOptions::default()
11509 .with_action(name)
11510 .with_text_style(text(12.0, color(220, 228, 238)));
11511 options.layout = LayoutStyle::new()
11512 .with_width(112.0)
11513 .with_height(24.0)
11514 .with_flex_shrink(0.0);
11515 widgets::checkbox(ui, parent, name, label, checked, options);
11516}
11517
11518fn shader_lab_slider_control(
11519 ui: &mut UiDocument,
11520 parent: UiNodeId,
11521 name: &'static str,
11522 label: &'static str,
11523 value: f32,
11524 max: f32,
11525 decimals: usize,
11526) {
11527 let control = ui.add_child(
11528 parent,
11529 UiNode::container(
11530 format!("{name}.control"),
11531 Layout::row()
11532 .size(LayoutSize::new(
11533 LayoutDimension::points(214.0),
11534 LayoutDimension::Auto,
11535 ))
11536 .align_items(LayoutAlignment::Center)
11537 .gap(LayoutGap::points(6.0, 6.0))
11538 .flex(0.0, 0.0, LayoutDimension::Auto)
11539 .to_layout_style(),
11540 ),
11541 );
11542 widgets::label(
11543 ui,
11544 control,
11545 format!("{name}.label"),
11546 label,
11547 text(12.0, color(166, 176, 190)),
11548 LayoutStyle::new()
11549 .with_width(46.0)
11550 .with_height(22.0)
11551 .with_flex_shrink(0.0),
11552 );
11553 let mut options = widgets::SliderOptions::default()
11554 .with_layout(
11555 LayoutStyle::new()
11556 .with_width(96.0)
11557 .with_height(22.0)
11558 .with_flex_shrink(0.0),
11559 )
11560 .with_value_edit_action(name);
11561 options.accessibility_label = Some(format!("Shader lab {label}"));
11562 widgets::slider(
11563 ui,
11564 control,
11565 format!("{name}.slider"),
11566 (value / max.max(f32::EPSILON)).clamp(0.0, 1.0),
11567 0.0..1.0,
11568 options,
11569 );
11570 widgets::label(
11571 ui,
11572 control,
11573 format!("{name}.value"),
11574 format!("{value:.decimals$}px"),
11575 text(12.0, color(226, 232, 242)),
11576 LayoutStyle::new()
11577 .with_width(48.0)
11578 .with_height(22.0)
11579 .with_flex_shrink(0.0),
11580 );
11581}
11582
11583fn shader_lab_surface_stroke(state: &ShowcaseState) -> Option<StrokeStyle> {
11584 (state.shader_lab_surface_stroke_width > f32::EPSILON).then(|| {
11585 StrokeStyle::new(
11586 color(150, 180, 235),
11587 state
11588 .shader_lab_surface_stroke_width
11589 .clamp(0.0, SHADER_LAB_SURFACE_STROKE_MAX),
11590 )
11591 })
11592}
11593
11594fn shader_lab_surface_radius(state: &ShowcaseState) -> f32 {
11595 state
11596 .shader_lab_surface_radius
11597 .clamp(0.0, SHADER_LAB_SURFACE_RADIUS_MAX)
11598}
11599
11600fn shader_lab_editor_column(
11601 ui: &mut UiDocument,
11602 parent: UiNodeId,
11603 state: &ShowcaseState,
11604 source_error: Option<&str>,
11605) {
11606 let editor_column = ui.add_child(
11607 parent,
11608 UiNode::container(
11609 "shader_lab.editor.column",
11610 LayoutStyle::column()
11611 .with_width_percent(1.0)
11612 .with_height_percent(1.0)
11613 .with_gap(8.0)
11614 .with_flex_shrink(1.0),
11615 ),
11616 );
11617 let preset_row = row(ui, editor_column, "shader_lab.preset.row", 8.0);
11618 widgets::label(
11619 ui,
11620 preset_row,
11621 "shader_lab.preset.caption",
11622 "Program",
11623 text(12.0, color(166, 176, 190)),
11624 LayoutStyle::new()
11625 .with_width(62.0)
11626 .with_height(30.0)
11627 .with_flex_shrink(0.0),
11628 );
11629 shader_lab_dropdown_select(
11630 ui,
11631 preset_row,
11632 "shader_lab.preset",
11633 &shader_lab_preset_options(),
11634 &state.shader_lab_preset_menu,
11635 180.0,
11636 "WGSL preset",
11637 "Shader lab WGSL preset",
11638 );
11639
11640 let editor_frame = ui.add_child(
11641 editor_column,
11642 UiNode::container(
11643 "shader_lab.editor.frame",
11644 Layout::column()
11645 .size(LayoutSize::new(
11646 LayoutDimension::percent(1.0),
11647 LayoutDimension::points(0.0),
11648 ))
11649 .min_size(LayoutSize::points(0.0, SHADER_LAB_EDITOR_HEIGHT))
11650 .flex(1.0, 1.0, LayoutDimension::points(0.0))
11651 .to_layout_style(),
11652 )
11653 .with_visual(UiVisual::panel(
11654 color(18, 22, 28),
11655 Some(StrokeStyle::new(color(72, 84, 104), 1.0)),
11656 4.0,
11657 )),
11658 );
11659 let editor_scroll = widgets::scroll_area(
11660 ui,
11661 editor_frame,
11662 "shader_lab.editor.scroll",
11663 ScrollAxes::BOTH,
11664 LayoutStyle::column()
11665 .with_width_percent(1.0)
11666 .with_height_percent(1.0),
11667 );
11668 ui.node_mut(editor_scroll)
11669 .set_action("shader_lab.editor.scroll");
11670 if let Some(scroll) = ui.node_mut(editor_scroll).scroll_mut() {
11671 scroll.set_offset(state.shader_lab_editor_scroll);
11672 }
11673
11674 let mut code_options = state.text_edit_options(FocusedTextInput::ShaderLabSource);
11675 code_options.edit_action = Some("shader_lab.editor.edit".into());
11676 code_options.visual = UiVisual::TRANSPARENT;
11677 code_options.focused_visual = Some(UiVisual::TRANSPARENT);
11678 code_options.disabled_visual = Some(UiVisual::TRANSPARENT);
11679 widgets::code_editor(
11680 ui,
11681 editor_scroll,
11682 "shader_lab.editor",
11683 &state.shader_lab_source,
11684 code_options,
11685 );
11686 let (validation_text, validation_color) = shader_lab_validation_label(source_error);
11687 widgets::label(
11688 ui,
11689 editor_column,
11690 "shader_lab.validation",
11691 validation_text,
11692 text(11.0, validation_color),
11693 LayoutStyle::new().with_width_percent(1.0),
11694 );
11695}
11696
11697fn shader_lab_editor_content_size(source: &str) -> UiSize {
11698 let style = widgets::code_text_style();
11699 let line_count = source.lines().count().max(1) as f32;
11700 let longest_line = source
11701 .lines()
11702 .map(|line| line.chars().count())
11703 .max()
11704 .unwrap_or(0)
11705 .max(48) as f32;
11706 UiSize::new(
11707 (longest_line * style.font_size * 0.56 + 24.0).max(SHADER_LAB_EDITOR_WIDTH),
11708 (line_count * style.line_height + 18.0).max(SHADER_LAB_EDITOR_HEIGHT),
11709 )
11710}
11711
11712fn shader_lab_preview(
11713 ui: &mut UiDocument,
11714 parent: UiNodeId,
11715 state: &ShowcaseState,
11716 source_valid: bool,
11717) {
11718 match state.shader_lab_target {
11719 ShaderLabTarget::Canvas => {
11720 let mut options = widgets::CanvasOptions::default()
11721 .with_layout(
11722 LayoutStyle::new()
11723 .with_width_percent(1.0)
11724 .with_height_percent(1.0)
11725 .with_flex_grow(1.0),
11726 )
11727 .with_intrinsic_size(UiSize::new(
11728 SHADER_LAB_PREVIEW_WIDTH - 20.0,
11729 SHADER_LAB_PREVIEW_HEIGHT,
11730 ))
11731 .with_accessibility_label("Shader lab canvas preview");
11732 options.visual = UiVisual::panel(color(8, 12, 18), None, 4.0);
11733 widgets::canvas(
11734 ui,
11735 parent,
11736 "shader_lab.preview.canvas",
11737 CanvasContent::new("shader_lab.preview.canvas")
11738 .program(shader_lab_canvas_program(state, source_valid)),
11739 options,
11740 );
11741 }
11742 ShaderLabTarget::Frame => {
11743 let mut frame_node = UiNode::container(
11744 "shader_lab.preview.frame",
11745 operad::layout::with_min_size(
11746 LayoutStyle::column()
11747 .with_width_percent(0.82)
11748 .with_height_percent(0.62)
11749 .with_padding(14.0)
11750 .with_align_items(taffy::prelude::AlignItems::Center)
11751 .with_justify_content(taffy::prelude::JustifyContent::Center)
11752 .with_flex_shrink(0.0),
11753 operad::layout::px(SHADER_LAB_FRAME_MIN_WIDTH),
11754 operad::layout::px(SHADER_LAB_FRAME_MIN_HEIGHT),
11755 ),
11756 )
11757 .with_visual(UiVisual::panel(
11758 ColorRgba::new(0, 0, 0, 0),
11759 shader_lab_surface_stroke(state),
11760 shader_lab_surface_radius(state),
11761 ));
11762 frame_node.style_mut().set_clip(ClipBehavior::Clip);
11763 let frame = ui.add_child(parent, frame_node);
11764 shader_lab_canvas_layer_fill(
11765 ui,
11766 frame,
11767 "shader_lab.preview.frame.shader",
11768 state,
11769 source_valid,
11770 );
11771 if state.shader_lab_show_frame_text {
11772 let label = widgets::label(
11773 ui,
11774 frame,
11775 "shader_lab.preview.frame.label",
11776 "WGSL frame",
11777 text(14.0, color(246, 249, 252)),
11778 LayoutStyle::new().with_flex_shrink(0.0),
11779 );
11780 ui.node_mut(label).style_mut().set_z_index(1);
11781 }
11782 }
11783 ShaderLabTarget::Button => {
11784 let mut shell_node = UiNode::container(
11785 "shader_lab.preview.button.shell",
11786 LayoutStyle::column()
11787 .with_width(SHADER_LAB_BUTTON_WIDTH)
11788 .with_height(SHADER_LAB_BUTTON_HEIGHT)
11789 .with_align_items(taffy::prelude::AlignItems::Center)
11790 .with_justify_content(taffy::prelude::JustifyContent::Center),
11791 )
11792 .with_visual(UiVisual::TRANSPARENT);
11793 shell_node.style_mut().set_clip(ClipBehavior::Clip);
11794 let shell = ui.add_child(parent, shell_node);
11795 shader_lab_canvas_layer_fill(
11796 ui,
11797 shell,
11798 "shader_lab.preview.button.shader",
11799 state,
11800 source_valid,
11801 );
11802 let mut options = widgets::ButtonOptions::new(
11803 LayoutStyle::new()
11804 .with_width(SHADER_LAB_BUTTON_WIDTH)
11805 .with_height(SHADER_LAB_BUTTON_HEIGHT),
11806 )
11807 .with_action("shader_lab.preview.button")
11808 .with_accessibility_label("Shader button preview");
11809 options.visual = UiVisual::panel(
11810 ColorRgba::new(0, 0, 0, 0),
11811 shader_lab_surface_stroke(state),
11812 shader_lab_surface_radius(state),
11813 );
11814 options.hovered_visual = Some(UiVisual::panel(
11815 ColorRgba::new(255, 255, 255, 28),
11816 shader_lab_surface_stroke(state),
11817 shader_lab_surface_radius(state),
11818 ));
11819 options.pressed_visual = Some(UiVisual::panel(
11820 ColorRgba::new(0, 0, 0, 48),
11821 shader_lab_surface_stroke(state),
11822 shader_lab_surface_radius(state),
11823 ));
11824 options.text_style = text(14.0, color(246, 249, 252));
11825 let button = widgets::button(
11826 ui,
11827 shell,
11828 "shader_lab.preview.button",
11829 if state.shader_lab_show_button_text {
11830 "Shader button"
11831 } else {
11832 ""
11833 },
11834 options,
11835 );
11836 ui.node_mut(button).style_mut().set_z_index(1);
11837 }
11838 }
11839}
11840
11841fn shader_lab_canvas_layer_fill(
11842 ui: &mut UiDocument,
11843 parent: UiNodeId,
11844 name: &'static str,
11845 state: &ShowcaseState,
11846 source_valid: bool,
11847) -> UiNodeId {
11848 let layout = operad::layout::with_absolute_position(
11849 LayoutStyle::new()
11850 .with_width_percent(1.0)
11851 .with_height_percent(1.0),
11852 0.0,
11853 0.0,
11854 );
11855 let mut options = widgets::CanvasOptions::default()
11856 .with_layout(layout)
11857 .with_intrinsic_size(UiSize::new(
11858 SHADER_LAB_PREVIEW_WIDTH,
11859 SHADER_LAB_PREVIEW_HEIGHT,
11860 ))
11861 .with_accessibility_label(format!("{name} shader preview"));
11862 options.input = InputBehavior::NONE;
11863 options.visual = UiVisual::TRANSPARENT;
11864 let canvas = widgets::canvas(
11865 ui,
11866 parent,
11867 name,
11868 CanvasContent::new(name).program(shader_lab_canvas_program(state, source_valid)),
11869 options,
11870 );
11871 ui.node_mut(canvas).style_mut().set_z_index(0);
11872 canvas
11873}
11874
11875fn shader_lab_status_label(state: &ShowcaseState, source_error: Option<&str>) -> Option<String> {
11876 if source_error.is_some() {
11877 Some(format!(
11878 "{} fallback until WGSL validates",
11879 state.shader_lab_target.label()
11880 ))
11881 } else {
11882 None
11883 }
11884}
11885
11886fn shader_lab_validation_label(source_error: Option<&str>) -> (String, ColorRgba) {
11887 if let Some(error) = source_error {
11888 (
11889 format!("WGSL error: {}", compact_shader_error(error, 160)),
11890 color(255, 139, 128),
11891 )
11892 } else {
11893 ("WGSL valid".to_string(), color(112, 221, 160))
11894 }
11895}
11896
11897fn compact_shader_error(error: &str, max_chars: usize) -> String {
11898 let mut compact = error.split_whitespace().collect::<Vec<_>>().join(" ");
11899 if compact.chars().count() > max_chars {
11900 compact = compact.chars().take(max_chars.saturating_sub(3)).collect();
11901 compact.push_str("...");
11902 }
11903 compact
11904}
11905
11906fn shader_lab_canvas_program(state: &ShowcaseState, source_valid: bool) -> CanvasRenderProgram {
11907 let source = if source_valid {
11908 state.shader_lab_source.text().to_string()
11909 } else {
11910 SHADER_LAB_ERROR_WGSL.to_string()
11911 };
11912 CanvasRenderProgram::wgsl(source)
11913 .label("showcase.shader_lab.canvas")
11914 .constant("TIME", state.progress_phase as f64)
11915 .clear_color(Some(color(8, 12, 18)))
11916}
11917
11918fn shader_lab_source_error(source: &str) -> Option<String> {
11919 if !shader_lab_source_has_entry_points(source) {
11920 return Some("source must define @vertex fn vs_main and @fragment fn fs_main".to_string());
11921 }
11922
11923 shader_lab_compile_error(source)
11924}
11925
11926#[cfg(feature = "wgpu")]
11927fn shader_lab_compile_error(source: &str) -> Option<String> {
11928 let module = match naga::front::wgsl::parse_str(source) {
11929 Ok(module) => module,
11930 Err(error) => return Some(error.emit_to_string(source)),
11931 };
11932 let mut validator = naga::valid::Validator::new(
11933 naga::valid::ValidationFlags::all(),
11934 naga::valid::Capabilities::empty(),
11935 );
11936 validator
11937 .validate(&module)
11938 .err()
11939 .map(|error| error.to_string())
11940}
11941
11942#[cfg(not(feature = "wgpu"))]
11943fn shader_lab_compile_error(_source: &str) -> Option<String> {
11944 None
11945}
11946
11947fn shader_lab_source_has_entry_points(source: &str) -> bool {
11948 source.contains("@vertex")
11949 && source.contains("fn vs_main")
11950 && source.contains("@fragment")
11951 && source.contains("fn fs_main")
11952}
11953
11954fn timeline_ruler(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
11955 let body =
11956 section_with_min_viewport(ui, parent, "timeline", "Timeline", UiSize::new(560.0, 0.0));
11957 widgets::label(
11958 ui,
11959 body,
11960 "timeline.label",
11961 "Clip timeline",
11962 text(12.0, color(166, 176, 190)),
11963 LayoutStyle::new().with_width_percent(1.0),
11964 );
11965 widgets::label(
11966 ui,
11967 body,
11968 "timeline.description",
11969 "The ruler maps time to tracks, clips, markers, and the current playhead.",
11970 text(12.0, color(196, 210, 230)),
11971 LayoutStyle::new().with_width_percent(1.0),
11972 );
11973
11974 let editor = row(ui, body, "timeline.editor", 0.0);
11975 let labels = ui.add_child(
11976 editor,
11977 UiNode::container(
11978 "timeline.lane_labels",
11979 LayoutStyle::column()
11980 .with_width(96.0)
11981 .with_height(172.0)
11982 .with_flex_shrink(0.0),
11983 ),
11984 );
11985 for (name, label, height) in [
11986 ("timeline.lane_labels.header", "Tracks", 40.0),
11987 ("timeline.lane_labels.video", "Video", 44.0),
11988 ("timeline.lane_labels.audio", "Audio", 44.0),
11989 ("timeline.lane_labels.notes", "Notes", 44.0),
11990 ] {
11991 widgets::label(
11992 ui,
11993 labels,
11994 name,
11995 label,
11996 text(11.0, color(166, 176, 190)),
11997 LayoutStyle::new()
11998 .with_width_percent(1.0)
11999 .with_height(height)
12000 .with_flex_shrink(0.0),
12001 );
12002 }
12003
12004 let timeline_scroll = timeline_scroll_state_for_view(
12005 state.timeline_scroll,
12006 state.timeline_scroll.viewport_size().width,
12007 );
12008 let range = ext_widgets::TimelineRange::new(0.0, 48.0);
12009 let nodes = scroll_area_widgets::scroll_container_shell(
12010 ui,
12011 editor,
12012 "timeline",
12013 timeline_scroll,
12014 widgets::ScrollContainerOptions::default()
12015 .with_axes(ScrollAxes::HORIZONTAL)
12016 .with_action_prefix("timeline")
12017 .with_gap(4.0)
12018 .with_scrollbar_thickness(TIMELINE_SCROLLBAR_HEIGHT)
12019 .with_layout(
12020 LayoutStyle::column()
12021 .with_width(0.0)
12022 .with_flex_grow(1.0)
12023 .with_height(TIMELINE_SCROLL_CONTAINER_HEIGHT)
12024 .with_flex_shrink(0.0),
12025 )
12026 .with_viewport_layout(
12027 LayoutStyle::column()
12028 .with_width(0.0)
12029 .with_flex_grow(1.0)
12030 .with_height(TIMELINE_VIEWPORT_HEIGHT)
12031 .with_flex_shrink(1.0),
12032 )
12033 .with_horizontal_scrollbar(
12034 scrollbar_widgets::ScrollbarOptions::default()
12035 .with_action("timeline.horizontal-scrollbar"),
12036 )
12037 .with_accessibility_label("Timeline horizontal scroller"),
12038 );
12039 let content = ui.add_child(
12040 nodes.viewport,
12041 UiNode::container(
12042 "timeline.content",
12043 LayoutStyle::column()
12044 .with_width(TIMELINE_CONTENT_WIDTH)
12045 .with_height(TIMELINE_VIEWPORT_HEIGHT)
12046 .with_flex_shrink(0.0),
12047 ),
12048 );
12049 let mut ruler_options = ext_widgets::TimelineRulerOptions::default();
12050 ruler_options.height = 40.0;
12051 ruler_options.layout = LayoutStyle::new()
12052 .with_width(TIMELINE_CONTENT_WIDTH)
12053 .with_height(40.0)
12054 .with_flex_shrink(0.0);
12055 ruler_options.accessibility_label = Some("Editing timeline ruler".to_string());
12056 ruler_options.accessibility_hint =
12057 Some("Shows seconds for the visible timeline clips".to_string());
12058 ext_widgets::timeline_ruler(
12059 ui,
12060 content,
12061 "timeline.ruler",
12062 ext_widgets::RulerSpec {
12063 range,
12064 width: TIMELINE_CONTENT_WIDTH,
12065 major_step: 4.0,
12066 minor_step: 1.0,
12067 label_every: 1,
12068 },
12069 ruler_options,
12070 );
12071 ui.add_child(
12072 content,
12073 UiNode::scene(
12074 "timeline.tracks",
12075 timeline_track_primitives(range, TIMELINE_CONTENT_WIDTH),
12076 LayoutStyle::new()
12077 .with_width(TIMELINE_CONTENT_WIDTH)
12078 .with_height(132.0)
12079 .with_flex_shrink(0.0),
12080 ),
12081 );
12082}
12083
12084fn timeline_track_primitives(range: ext_widgets::TimelineRange, width: f32) -> Vec<ScenePrimitive> {
12085 let mut primitives = Vec::new();
12086 let lane_height = 36.0;
12087 let lane_gap = 8.0;
12088 let lanes = [
12089 ("Video", 0.0, color(16, 22, 30)),
12090 ("Audio", lane_height + lane_gap, color(13, 20, 27)),
12091 ("Notes", (lane_height + lane_gap) * 2.0, color(16, 22, 30)),
12092 ];
12093
12094 for (label, y, fill) in lanes {
12095 primitives.push(ScenePrimitive::Rect(
12096 PaintRect::solid(UiRect::new(0.0, y, width, lane_height), fill)
12097 .stroke(AlignedStroke::inside(StrokeStyle::new(
12098 color(38, 49, 64),
12099 1.0,
12100 )))
12101 .corner_radii(CornerRadii::uniform(2.0)),
12102 ));
12103 primitives.push(ScenePrimitive::Text(
12104 PaintText::new(
12105 label,
12106 UiRect::new(8.0, y + 8.0, 72.0, 18.0),
12107 text(10.0, color(116, 128, 145)),
12108 )
12109 .multiline(false),
12110 ));
12111 }
12112
12113 for second in (0..=48).step_by(4) {
12114 let x = range.value_to_x(second as f64, width);
12115 primitives.push(ScenePrimitive::Line {
12116 from: UiPoint::new(x, 0.0),
12117 to: UiPoint::new(x, 124.0),
12118 stroke: StrokeStyle::new(color(34, 44, 58), 1.0),
12119 });
12120 }
12121
12122 push_timeline_clip(
12123 &mut primitives,
12124 range,
12125 width,
12126 "Intro",
12127 2.0,
12128 10.0,
12129 0.0,
12130 color(57, 126, 207),
12131 );
12132 push_timeline_clip(
12133 &mut primitives,
12134 range,
12135 width,
12136 "Cutaway",
12137 12.0,
12138 24.0,
12139 0.0,
12140 color(95, 107, 212),
12141 );
12142 push_timeline_clip(
12143 &mut primitives,
12144 range,
12145 width,
12146 "Final",
12147 28.0,
12148 44.0,
12149 0.0,
12150 color(68, 153, 122),
12151 );
12152 push_timeline_clip(
12153 &mut primitives,
12154 range,
12155 width,
12156 "Music bed",
12157 0.0,
12158 48.0,
12159 lane_height + lane_gap,
12160 color(205, 160, 71),
12161 );
12162 push_timeline_clip(
12163 &mut primitives,
12164 range,
12165 width,
12166 "Voiceover",
12167 8.0,
12168 18.0,
12169 lane_height + lane_gap,
12170 color(183, 107, 185),
12171 );
12172
12173 for (second, label) in [(6.0, "Beat"), (21.0, "Cut"), (37.0, "Cue")] {
12174 let x = range.value_to_x(second, width);
12175 let y = (lane_height + lane_gap) * 2.0 + 8.0;
12176 primitives.push(ScenePrimitive::Polygon {
12177 points: vec![
12178 UiPoint::new(x, y),
12179 UiPoint::new(x + 8.0, y + 8.0),
12180 UiPoint::new(x, y + 16.0),
12181 UiPoint::new(x - 8.0, y + 8.0),
12182 ],
12183 fill: color(245, 198, 83),
12184 stroke: Some(StrokeStyle::new(color(255, 234, 178), 1.0)),
12185 });
12186 primitives.push(ScenePrimitive::Text(
12187 PaintText::new(
12188 label,
12189 UiRect::new(x + 12.0, y - 1.0, 72.0, 18.0),
12190 text(10.0, color(225, 233, 244)),
12191 )
12192 .multiline(false),
12193 ));
12194 }
12195
12196 let playhead_x = range.value_to_x(18.5, width);
12197 primitives.push(ScenePrimitive::Line {
12198 from: UiPoint::new(playhead_x, 0.0),
12199 to: UiPoint::new(playhead_x, 124.0),
12200 stroke: StrokeStyle::new(ColorRgba::new(255, 120, 96, 255), 2.0),
12201 });
12202 primitives.push(ScenePrimitive::Text(
12203 PaintText::new(
12204 "Playhead 18.5s",
12205 UiRect::new(playhead_x + 8.0, 106.0, 120.0, 18.0),
12206 text(10.0, ColorRgba::new(255, 172, 154, 255)),
12207 )
12208 .multiline(false),
12209 ));
12210
12211 primitives
12212}
12213
12214#[allow(clippy::too_many_arguments)]
12215fn push_timeline_clip(
12216 primitives: &mut Vec<ScenePrimitive>,
12217 range: ext_widgets::TimelineRange,
12218 width: f32,
12219 label: &'static str,
12220 start: f64,
12221 end: f64,
12222 lane_y: f32,
12223 fill: ColorRgba,
12224) {
12225 let x = range.value_to_x(start, width);
12226 let right = range.value_to_x(end, width);
12227 let rect = UiRect::new(x, lane_y + 6.0, (right - x).max(1.0), 24.0);
12228 primitives.push(ScenePrimitive::Rect(
12229 PaintRect::solid(rect, fill)
12230 .stroke(AlignedStroke::inside(StrokeStyle::new(
12231 ColorRgba::new(230, 240, 255, 96),
12232 1.0,
12233 )))
12234 .corner_radii(CornerRadii::uniform(4.0)),
12235 ));
12236 primitives.push(ScenePrimitive::Text(
12237 PaintText::new(label, rect, text(10.0, color(245, 248, 252)))
12238 .horizontal_align(TextHorizontalAlign::Center)
12239 .vertical_align(TextVerticalAlign::Center)
12240 .multiline(false),
12241 ));
12242}
12243
12244fn theme_demo_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState, theme: &Theme) {
12245 let body = section(ui, parent, "theme", "Theme");
12246 widgets::label(
12247 ui,
12248 body,
12249 "theme.current",
12250 format!("Current theme: {}", theme.name),
12251 themed_text(theme, 14.0),
12252 LayoutStyle::new().with_width_percent(1.0),
12253 );
12254
12255 let choices = wrapping_row(ui, body, "theme.choices", 8.0);
12256 for choice in [
12257 ShowcaseThemeChoice::Light,
12258 ShowcaseThemeChoice::Dark,
12259 ShowcaseThemeChoice::Bubblegum,
12260 ] {
12261 theme_choice_button(
12262 ui,
12263 choices,
12264 choice,
12265 state.showcase_theme == choice,
12266 choice.theme(),
12267 );
12268 }
12269
12270 let swatches = wrapping_row(ui, body, "theme.swatches", 8.0);
12271 theme_swatch(
12272 ui,
12273 swatches,
12274 "theme.swatch.canvas",
12275 "Canvas",
12276 theme.colors.canvas,
12277 theme,
12278 );
12279 theme_swatch(
12280 ui,
12281 swatches,
12282 "theme.swatch.surface",
12283 "Surface",
12284 theme.colors.surface,
12285 theme,
12286 );
12287 theme_swatch(
12288 ui,
12289 swatches,
12290 "theme.swatch.accent",
12291 "Accent",
12292 theme.colors.accent,
12293 theme,
12294 );
12295 theme_swatch(
12296 ui,
12297 swatches,
12298 "theme.swatch.selected",
12299 "Selected",
12300 theme.colors.selected,
12301 theme,
12302 );
12303
12304 let preview = ui.add_child(
12305 body,
12306 UiNode::container(
12307 "theme.preview",
12308 LayoutStyle::column()
12309 .with_width_percent(1.0)
12310 .with_padding(12.0)
12311 .with_gap(10.0)
12312 .with_flex_shrink(0.0),
12313 )
12314 .with_visual(UiVisual::panel(
12315 theme.colors.surface,
12316 Some(theme.stroke.surface),
12317 theme.radius.md,
12318 ))
12319 .with_accessibility(
12320 AccessibilityMeta::new(AccessibilityRole::Group).label("Theme preview"),
12321 ),
12322 );
12323 widgets::label(
12324 ui,
12325 preview,
12326 "theme.preview.title",
12327 "Preview controls",
12328 themed_text(theme, 13.0),
12329 LayoutStyle::new().with_width_percent(1.0),
12330 );
12331 let preview_row = row(ui, preview, "theme.preview.controls", 8.0);
12332 let mut primary = themed_button_options(
12333 theme,
12334 "theme.preview.primary",
12335 ComponentState::ACTIVE,
12336 LayoutStyle::new().with_height(34.0),
12337 );
12338 primary.accessibility_label = Some("Primary preview button".to_owned());
12339 widgets::button(ui, preview_row, "theme.preview.primary", "Primary", primary);
12340 let mut secondary = themed_button_options(
12341 theme,
12342 "theme.preview.secondary",
12343 ComponentState::NORMAL,
12344 LayoutStyle::new().with_height(34.0),
12345 );
12346 secondary.accessibility_label = Some("Secondary preview button".to_owned());
12347 widgets::button(
12348 ui,
12349 preview_row,
12350 "theme.preview.secondary",
12351 "Secondary",
12352 secondary,
12353 );
12354 let mut help = themed_muted_text(theme, 12.0);
12355 help.wrap = TextWrap::WordOrGlyph;
12356 widgets::label(
12357 ui,
12358 preview,
12359 "theme.preview.copy",
12360 "The selected theme drives the app background, right panel, floating windows, and this preview.",
12361 help,
12362 LayoutStyle::new().with_width_percent(1.0),
12363 );
12364}
12365
12366fn theme_choice_button(
12367 ui: &mut UiDocument,
12368 parent: UiNodeId,
12369 choice: ShowcaseThemeChoice,
12370 selected: bool,
12371 preview_theme: Theme,
12372) {
12373 let mut options = themed_button_options(
12374 &preview_theme,
12375 choice.action(),
12376 if selected {
12377 ComponentState::SELECTED
12378 } else {
12379 ComponentState::NORMAL
12380 },
12381 LayoutStyle::new()
12382 .with_width(116.0)
12383 .with_height(34.0)
12384 .with_flex_shrink(0.0),
12385 )
12386 .with_action(choice.action());
12387 options.accessibility_label = Some(format!("Use {} theme", choice.label()));
12388 widgets::button(
12389 ui,
12390 parent,
12391 format!("theme.choice.{}", choice.label().to_ascii_lowercase()),
12392 choice.label(),
12393 options,
12394 );
12395}
12396
12397fn theme_swatch(
12398 ui: &mut UiDocument,
12399 parent: UiNodeId,
12400 name: &'static str,
12401 label: &'static str,
12402 swatch_color: ColorRgba,
12403 theme: &Theme,
12404) {
12405 let tile = ui.add_child(
12406 parent,
12407 UiNode::container(
12408 name,
12409 LayoutStyle::column()
12410 .with_width(92.0)
12411 .with_height(76.0)
12412 .with_padding(8.0)
12413 .with_gap(6.0)
12414 .with_flex_shrink(0.0),
12415 )
12416 .with_visual(UiVisual::panel(
12417 theme.colors.surface_muted,
12418 Some(theme.stroke.surface),
12419 4.0,
12420 ))
12421 .with_accessibility(AccessibilityMeta::new(AccessibilityRole::Group).label(label)),
12422 );
12423 ui.add_child(
12424 tile,
12425 UiNode::container(
12426 format!("{name}.color"),
12427 LayoutStyle::new()
12428 .with_width_percent(1.0)
12429 .with_height(26.0)
12430 .with_flex_shrink(0.0),
12431 )
12432 .with_visual(UiVisual::panel(
12433 swatch_color,
12434 Some(StrokeStyle::new(theme.colors.border_strong, 1.0)),
12435 4.0,
12436 )),
12437 );
12438 widgets::label(
12439 ui,
12440 tile,
12441 format!("{name}.label"),
12442 label,
12443 themed_muted_text(theme, 11.0),
12444 LayoutStyle::new().with_width_percent(1.0),
12445 );
12446}
12447
12448fn themed_button_options(
12449 theme: &Theme,
12450 action: impl Into<String>,
12451 state: ComponentState,
12452 layout: LayoutStyle,
12453) -> widgets::ButtonOptions {
12454 let mut options = widgets::ButtonOptions::new(layout).with_action(action.into());
12455 options.visual = theme.resolve_visual(ComponentRole::Button, state);
12456 options.hovered_visual =
12457 Some(theme.resolve_visual(ComponentRole::Button, ComponentState::HOVERED));
12458 options.pressed_visual =
12459 Some(theme.resolve_visual(ComponentRole::Button, ComponentState::PRESSED));
12460 options.pressed_hovered_visual =
12461 Some(theme.resolve_visual(ComponentRole::Button, ComponentState::PRESSED));
12462 options.focused_visual =
12463 Some(theme.resolve_visual(ComponentRole::Button, ComponentState::FOCUSED));
12464 options.disabled_visual =
12465 Some(theme.resolve_visual(ComponentRole::Button, ComponentState::DISABLED));
12466 options.text_style = theme.resolve_text(ComponentRole::Button, state);
12467 options
12468}
12469
12470fn styling_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12471 let preview_scene_size = style_preview_scene_size(state.styling);
12472 let preview_min_width = preview_scene_size.width + 16.0;
12473 let preview_min_height = preview_scene_size.height + 16.0;
12474 let body_min_width = STYLING_CONTROLS_WIDTH + 1.0 + preview_min_width + 20.0;
12475 let body = section_with_min_viewport(
12476 ui,
12477 parent,
12478 "styling",
12479 "Styling",
12480 UiSize::new(body_min_width, preview_min_height),
12481 );
12482 let grid_layout = operad::layout::with_grid_template_columns(
12483 Layout::grid()
12484 .size(LayoutSize::percent(1.0, 1.0))
12485 .gap(LayoutGap::points(10.0, 10.0))
12486 .to_layout_style(),
12487 [
12488 LayoutGridTrack::points(STYLING_CONTROLS_WIDTH),
12489 LayoutGridTrack::points(1.0),
12490 LayoutGridTrack::minmax_points_fraction(preview_min_width, 1.0),
12491 ],
12492 );
12493 let grid = ui.add_child(body, UiNode::container("styling.grid", grid_layout));
12494 let controls = ui.add_child(
12495 grid,
12496 UiNode::container(
12497 "styling.controls",
12498 LayoutStyle::column()
12499 .with_width(STYLING_CONTROLS_WIDTH)
12500 .with_height_percent(1.0)
12501 .with_flex_shrink(0.0)
12502 .gap(6.0),
12503 ),
12504 );
12505 style_edge_group(
12506 ui,
12507 controls,
12508 "styling.inner",
12509 "Inner margin",
12510 "styling.inner_same",
12511 state.styling.inner_same,
12512 [
12513 ("Left", "styling.inner", state.styling.inner_margin),
12514 ("Right", "styling.inner_right", state.styling.inner_right),
12515 ("Top", "styling.inner_top", state.styling.inner_top),
12516 ("Bottom", "styling.inner_bottom", state.styling.inner_bottom),
12517 ],
12518 0.0..32.0,
12519 );
12520 style_edge_group(
12521 ui,
12522 controls,
12523 "styling.outer",
12524 "Outer margin",
12525 "styling.outer_same",
12526 state.styling.outer_same,
12527 [
12528 ("Left", "styling.outer", state.styling.outer_margin),
12529 ("Right", "styling.outer_right", state.styling.outer_right),
12530 ("Top", "styling.outer_top", state.styling.outer_top),
12531 ("Bottom", "styling.outer_bottom", state.styling.outer_bottom),
12532 ],
12533 0.0..40.0,
12534 );
12535 style_edge_group(
12536 ui,
12537 controls,
12538 "styling.radius",
12539 "Corner radius",
12540 "styling.radius_same",
12541 state.styling.radius_same,
12542 [
12543 ("NW", "styling.radius", state.styling.corner_radius),
12544 ("NE", "styling.radius_ne", state.styling.corner_ne),
12545 ("SW", "styling.radius_sw", state.styling.corner_sw),
12546 ("SE", "styling.radius_se", state.styling.corner_se),
12547 ],
12548 0.0..28.0,
12549 );
12550 style_fill_group(ui, controls, state);
12551 style_stroke_group(ui, controls, state);
12552 style_shadow_group(ui, controls, state);
12553 widgets::separator(
12554 ui,
12555 grid,
12556 "styling.preview.separator",
12557 widgets::SeparatorOptions::vertical().with_layout(
12558 LayoutStyle::new()
12559 .with_width(1.0)
12560 .with_height_percent(1.0)
12561 .with_flex_shrink(0.0),
12562 ),
12563 );
12564
12565 let preview = ui.add_child(
12566 grid,
12567 UiNode::container(
12568 "styling.preview",
12569 operad::layout::with_min_size(
12570 LayoutStyle::column()
12571 .with_width_percent(1.0)
12572 .with_height_percent(1.0)
12573 .with_flex_shrink(0.0)
12574 .padding(8.0),
12575 operad::layout::px(preview_min_width),
12576 operad::layout::px(preview_min_height),
12577 ),
12578 )
12579 .with_visual(UiVisual::panel(color(17, 20, 25), None, 0.0)),
12580 );
12581 style_preview(ui, preview, state.styling);
12582}
12583
12584#[allow(clippy::too_many_arguments)]
12585fn style_edge_group(
12586 ui: &mut UiDocument,
12587 parent: UiNodeId,
12588 name: &'static str,
12589 title: &'static str,
12590 same_action: &'static str,
12591 same: bool,
12592 values: [(&'static str, &'static str, f32); 4],
12593 range: std::ops::Range<f32>,
12594) {
12595 let group = style_control_group(ui, parent, format!("{name}.group"));
12596 style_group_title(ui, group, format!("{name}.title"), title);
12597 let fields = ui.add_child(
12598 group,
12599 UiNode::container(
12600 format!("{name}.fields"),
12601 LayoutStyle::column()
12602 .with_width(138.0)
12603 .with_flex_shrink(0.0)
12604 .gap(3.0),
12605 ),
12606 );
12607 style_compact_checkbox(ui, fields, same_action, "same", same);
12608 if same {
12609 style_number_row(ui, fields, values[0].1, "All", values[0].2, range, 0);
12610 } else {
12611 for (label, action, value) in values {
12612 style_number_row(ui, fields, action, label, value, range.clone(), 0);
12613 }
12614 }
12615}
12616
12617fn style_fill_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12618 let group = style_control_group(ui, parent, "styling.fill.group");
12619 style_group_title(ui, group, "styling.fill.title", "Fill");
12620 let fields = style_group_fields(
12621 ui,
12622 group,
12623 "styling.fill.fields",
12624 STYLING_WIDE_FIELDS_WIDTH,
12625 4.0,
12626 );
12627 style_color_button_row(
12628 ui,
12629 fields,
12630 "styling.fill_color_button",
12631 "",
12632 state.styling.fill_color(),
12633 "Pick fill color",
12634 );
12635 if state.styling_fill_picker_open {
12636 ext_widgets::color_picker(
12637 ui,
12638 fields,
12639 "styling.fill_picker",
12640 &state.styling_fill_picker,
12641 ext_widgets::ColorPickerOptions::default()
12642 .with_label("Fill")
12643 .with_action_prefix("styling.fill_picker"),
12644 );
12645 }
12646}
12647
12648fn style_stroke_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12649 let group = style_control_group(ui, parent, "styling.stroke.group");
12650 style_group_title(ui, group, "styling.stroke.title", "Stroke");
12651 let fields = style_group_fields(
12652 ui,
12653 group,
12654 "styling.stroke.fields",
12655 STYLING_WIDE_FIELDS_WIDTH,
12656 4.0,
12657 );
12658 let width_row = row(ui, fields, "styling.stroke.row", 6.0);
12659 style_inline_number(
12660 ui,
12661 width_row,
12662 "styling.stroke",
12663 "width",
12664 state.styling.stroke_width,
12665 0.0..STYLING_STROKE_MAX,
12666 1,
12667 );
12668 let mut options = widgets::SliderOptions::default()
12669 .with_layout(
12670 LayoutStyle::new()
12671 .with_width(60.0)
12672 .with_height(20.0)
12673 .with_flex_shrink(0.0),
12674 )
12675 .with_value_edit_action("styling.stroke");
12676 options.fill_color = color(120, 170, 230);
12677 widgets::slider(
12678 ui,
12679 width_row,
12680 "styling.stroke.slider",
12681 (state.styling.stroke_width / STYLING_STROKE_MAX).clamp(0.0, 1.0),
12682 0.0..1.0,
12683 options,
12684 );
12685 style_color_button_row(
12686 ui,
12687 fields,
12688 "styling.stroke_color_button",
12689 "",
12690 state.styling.stroke_color(),
12691 "Pick stroke color",
12692 );
12693 if state.styling_stroke_picker_open {
12694 ext_widgets::color_picker(
12695 ui,
12696 fields,
12697 "styling.stroke_picker",
12698 &state.styling_stroke_picker,
12699 ext_widgets::ColorPickerOptions::default()
12700 .with_label("Stroke color")
12701 .with_action_prefix("styling.stroke_picker"),
12702 );
12703 }
12704}
12705
12706fn style_shadow_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12707 let group = style_control_group(ui, parent, "styling.shadow.group");
12708 style_group_title(ui, group, "styling.shadow.title", "Shadow");
12709 let fields = style_group_fields(
12710 ui,
12711 group,
12712 "styling.shadow.fields",
12713 STYLING_WIDE_FIELDS_WIDTH,
12714 4.0,
12715 );
12716 let offsets = row(ui, fields, "styling.shadow.offsets", 6.0);
12717 style_inline_number(
12718 ui,
12719 offsets,
12720 "styling.shadow_x",
12721 "x",
12722 state.styling.shadow_x,
12723 -24.0..24.0,
12724 0,
12725 );
12726 style_inline_number(
12727 ui,
12728 offsets,
12729 "styling.shadow_y",
12730 "y",
12731 state.styling.shadow_y,
12732 -24.0..24.0,
12733 0,
12734 );
12735 let spread = row(ui, fields, "styling.shadow.blur_spread", 6.0);
12736 style_inline_number(
12737 ui,
12738 spread,
12739 "styling.shadow",
12740 "blur",
12741 state.styling.shadow_blur,
12742 0.0..32.0,
12743 0,
12744 );
12745 style_inline_number(
12746 ui,
12747 spread,
12748 "styling.shadow_spread",
12749 "spread",
12750 state.styling.shadow_spread,
12751 0.0..16.0,
12752 0,
12753 );
12754 style_color_button_row(
12755 ui,
12756 fields,
12757 "styling.shadow_color_button",
12758 "",
12759 state.styling.shadow_color(),
12760 "Pick shadow color",
12761 );
12762 if state.styling_shadow_picker_open {
12763 ext_widgets::color_picker(
12764 ui,
12765 fields,
12766 "styling.shadow_picker",
12767 &state.styling_shadow_picker,
12768 ext_widgets::ColorPickerOptions::default()
12769 .with_label("Shadow color")
12770 .with_action_prefix("styling.shadow_picker"),
12771 );
12772 }
12773}
12774
12775fn style_control_group(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>) -> UiNodeId {
12776 ui.add_child(
12777 parent,
12778 UiNode::container(
12779 name,
12780 LayoutStyle::row()
12781 .with_width_percent(1.0)
12782 .with_flex_shrink(0.0)
12783 .padding(4.0)
12784 .gap(8.0),
12785 )
12786 .with_visual(UiVisual::panel(color(23, 27, 33), None, 2.0)),
12787 )
12788}
12789
12790fn style_group_fields(
12791 ui: &mut UiDocument,
12792 parent: UiNodeId,
12793 name: impl Into<String>,
12794 width: f32,
12795 gap: f32,
12796) -> UiNodeId {
12797 ui.add_child(
12798 parent,
12799 UiNode::container(
12800 name,
12801 LayoutStyle::column()
12802 .with_width(width)
12803 .with_flex_shrink(0.0)
12804 .gap(gap),
12805 ),
12806 )
12807}
12808
12809fn style_group_title(
12810 ui: &mut UiDocument,
12811 parent: UiNodeId,
12812 name: impl Into<String>,
12813 label: &'static str,
12814) {
12815 widgets::label(
12816 ui,
12817 parent,
12818 name,
12819 label,
12820 text(12.0, color(166, 176, 190)),
12821 LayoutStyle::new()
12822 .with_width(88.0)
12823 .with_flex_shrink(0.0)
12824 .with_height(22.0),
12825 );
12826}
12827
12828fn style_color_button_row(
12829 ui: &mut UiDocument,
12830 parent: UiNodeId,
12831 action: &'static str,
12832 label: &'static str,
12833 value: ColorRgba,
12834 accessibility_label: &'static str,
12835) {
12836 let row = row(ui, parent, format!("{action}.row"), 8.0);
12837 if !label.is_empty() {
12838 widgets::label(
12839 ui,
12840 row,
12841 format!("{action}.label"),
12842 label,
12843 text(12.0, color(166, 176, 190)),
12844 LayoutStyle::new()
12845 .with_width(86.0)
12846 .with_flex_shrink(0.0)
12847 .with_height(24.0),
12848 );
12849 }
12850 ext_widgets::color_edit_button(
12851 ui,
12852 row,
12853 action,
12854 value,
12855 color_mini_button_options(action)
12856 .with_format(ext_widgets::ColorValueFormat::Rgba)
12857 .accessibility_label(accessibility_label),
12858 );
12859 widgets::label(
12860 ui,
12861 row,
12862 format!("{action}.value"),
12863 ext_widgets::color_picker::format_hex_color(value, value.a < 255),
12864 text(12.0, color(226, 232, 242)),
12865 LayoutStyle::new().with_width(96.0).with_height(24.0),
12866 );
12867}
12868
12869fn style_number_row(
12870 ui: &mut UiDocument,
12871 parent: UiNodeId,
12872 name: &'static str,
12873 label: &'static str,
12874 value: f32,
12875 range: std::ops::Range<f32>,
12876 decimals: u8,
12877) {
12878 let row = row(ui, parent, format!("{name}.row"), 6.0);
12879 widgets::label(
12880 ui,
12881 row,
12882 format!("{name}.label"),
12883 label,
12884 text(12.0, color(166, 176, 190)),
12885 LayoutStyle::new().with_width(48.0).with_height(22.0),
12886 );
12887 style_value_input(ui, row, name, value, range, decimals);
12888}
12889
12890fn style_inline_number(
12891 ui: &mut UiDocument,
12892 parent: UiNodeId,
12893 name: &'static str,
12894 label: &'static str,
12895 value: f32,
12896 range: std::ops::Range<f32>,
12897 decimals: u8,
12898) {
12899 let row = compact_row(ui, parent, format!("{name}.inline"), 3.0);
12900 widgets::label(
12901 ui,
12902 row,
12903 format!("{name}.inline_label"),
12904 format!("{label}:"),
12905 text(12.0, color(166, 176, 190)),
12906 LayoutStyle::new()
12907 .with_width(if label.len() > 1 { 42.0 } else { 16.0 })
12908 .with_height(22.0),
12909 );
12910 style_value_input(ui, row, name, value, range, decimals);
12911}
12912
12913fn style_value_input(
12914 ui: &mut UiDocument,
12915 parent: UiNodeId,
12916 name: &'static str,
12917 value: f32,
12918 range: std::ops::Range<f32>,
12919 decimals: u8,
12920) {
12921 let mut options = widgets::DragValueOptions::default()
12922 .with_layout(
12923 LayoutStyle::row()
12924 .with_width(STYLING_VALUE_INPUT_WIDTH)
12925 .with_height(22.0)
12926 .with_flex_shrink(0.0)
12927 .with_align_items(taffy::prelude::AlignItems::Center)
12928 .with_justify_content(taffy::prelude::JustifyContent::Center)
12929 .with_padding(4.0),
12930 )
12931 .with_range(ext_widgets::NumericRange::new(
12932 f64::from(range.start),
12933 f64::from(range.end),
12934 ))
12935 .with_precision(ext_widgets::NumericPrecision::decimals(decimals))
12936 .with_action(name);
12937 options.text_style = text(12.0, color(226, 232, 242));
12938 widgets::drag_value_input(ui, parent, name, f64::from(value), options);
12939}
12940
12941fn style_compact_checkbox(
12942 ui: &mut UiDocument,
12943 parent: UiNodeId,
12944 name: &'static str,
12945 label: &'static str,
12946 checked: bool,
12947) {
12948 let mut options = widgets::CheckboxOptions::default().with_action(name);
12949 options.layout = LayoutStyle::new().with_width(92.0).with_height(22.0);
12950 options.text_style = text(12.0, color(220, 228, 238));
12951 widgets::checkbox(ui, parent, name, label, checked, options);
12952}
12953
12954fn compact_row(
12955 ui: &mut UiDocument,
12956 parent: UiNodeId,
12957 name: impl Into<String>,
12958 gap: f32,
12959) -> UiNodeId {
12960 ui.add_child(
12961 parent,
12962 UiNode::container(
12963 name,
12964 LayoutStyle::row()
12965 .with_height(22.0)
12966 .with_flex_shrink(0.0)
12967 .with_align_items(taffy::prelude::AlignItems::Center)
12968 .gap(gap),
12969 ),
12970 )
12971}
12972
12973fn color_mini_button_options(action: &'static str) -> ext_widgets::ColorButtonOptions {
12974 ext_widgets::ColorButtonOptions::default()
12975 .with_layout(LayoutStyle::size(28.0, 24.0).with_flex_shrink(0.0))
12976 .with_swatch_size(UiSize::new(22.0, 18.0))
12977 .with_action(action)
12978 .show_label(false)
12979}
12980
12981fn style_preview(ui: &mut UiDocument, parent: UiNodeId, styling: StylingState) {
12982 let (frame, text_rect) = style_preview_rects(styling);
12983 let scene_size = style_preview_scene_size(styling);
12984 ui.add_child(
12985 parent,
12986 UiNode::scene(
12987 "styling.preview.scene",
12988 vec![
12989 ScenePrimitive::Rect(
12990 PaintRect::solid(frame, styling.fill_color())
12991 .stroke(AlignedStroke::inside(StrokeStyle::new(
12992 styling.stroke_color(),
12993 styling.stroke_width,
12994 )))
12995 .corner_radii(styling.radii())
12996 .effect(PaintEffect::shadow(
12997 styling.shadow_color(),
12998 UiPoint::new(styling.shadow_x, styling.shadow_y),
12999 styling.shadow_blur,
13000 styling.shadow_spread,
13001 )),
13002 ),
13003 ScenePrimitive::Text(
13004 PaintText::new("Content", text_rect, text(13.0, color(255, 255, 255)))
13005 .horizontal_align(TextHorizontalAlign::Center)
13006 .vertical_align(TextVerticalAlign::Center)
13007 .multiline(false),
13008 ),
13009 ],
13010 operad::layout::with_min_size(
13011 LayoutStyle::new()
13012 .with_width_percent(1.0)
13013 .with_height(180.0)
13014 .with_flex_shrink(0.0),
13015 operad::layout::px(scene_size.width),
13016 operad::layout::px(scene_size.height),
13017 ),
13018 ),
13019 );
13020}
13021
13022fn style_preview_rects(styling: StylingState) -> (UiRect, UiRect) {
13023 let outer = styling.outer_edges();
13024 let inner = styling.inner_edges();
13025 let frame = UiRect::new(
13026 22.0 + outer[0],
13027 28.0 + outer[2],
13028 108.0 + inner[0] + inner[1],
13029 40.0 + inner[2] + inner[3],
13030 );
13031 let text_rect = UiRect::new(
13032 frame.x + inner[0],
13033 frame.y + inner[2],
13034 (frame.width - inner[0] - inner[1]).max(1.0),
13035 (frame.height - inner[2] - inner[3]).max(1.0),
13036 );
13037 (frame, text_rect)
13038}
13039
13040fn style_preview_scene_size(styling: StylingState) -> UiSize {
13041 let (frame, text_rect) = style_preview_rects(styling);
13042 let shadow_outset = styling.shadow_blur.max(0.0) + styling.shadow_spread.max(0.0);
13043 let shadow_bounds = UiRect::new(
13044 frame.x + styling.shadow_x - shadow_outset,
13045 frame.y + styling.shadow_y - shadow_outset,
13046 frame.width + shadow_outset * 2.0,
13047 frame.height + shadow_outset * 2.0,
13048 );
13049 let right = frame
13050 .right()
13051 .max(text_rect.right())
13052 .max(shadow_bounds.right());
13053 let bottom = frame
13054 .bottom()
13055 .max(text_rect.bottom())
13056 .max(shadow_bounds.bottom())
13057 .max(180.0);
13058 UiSize::new(right.ceil().max(1.0), bottom.ceil().max(1.0))
13059}
13060
13061fn slider_options(state: &ShowcaseState, width: f32) -> widgets::SliderOptions {
13062 let mut options = widgets::SliderOptions::default().with_layout(
13063 LayoutStyle::new()
13064 .with_width(width)
13065 .with_height(24.0)
13066 .with_flex_shrink(0.0),
13067 );
13068 options.fill_color = if state.slider_trailing_color {
13069 state.slider_trailing_picker.value()
13070 } else {
13071 color(42, 49, 58)
13072 };
13073 options.thumb_shape = match state.slider_thumb_shape {
13074 SliderThumbChoice::Circle => widgets::slider::SliderThumbShape::Circle,
13075 SliderThumbChoice::Square => widgets::slider::SliderThumbShape::Square,
13076 SliderThumbChoice::Rectangle => widgets::slider::SliderThumbShape::Rectangle,
13077 };
13078 options.thumb_visual = UiVisual::panel(
13079 state.slider_thumb_picker.value(),
13080 Some(StrokeStyle::new(color(79, 93, 113), 1.0)),
13081 6.0,
13082 );
13083 options
13084}
13085
13086#[allow(clippy::field_reassign_with_default)]
13087fn slider_number_input(
13088 ui: &mut UiDocument,
13089 parent: UiNodeId,
13090 name: &'static str,
13091 input: &TextInputState,
13092 focused: FocusedTextInput,
13093 state: &ShowcaseState,
13094 width: f32,
13095) {
13096 let mut options = TextInputOptions::default();
13097 options.layout = LayoutStyle::new().with_width(width).with_height(28.0);
13098 options.text_style = text(12.0, color(230, 236, 246));
13099 options.placeholder_style = text(12.0, color(144, 156, 174));
13100 options.edit_action = Some(format!("{name}.edit").into());
13101 options.focused = state.focused_text == Some(focused);
13102 options.caret_visible = caret_visible(state.caret_phase);
13103 widgets::text_input(ui, parent, name, input, options);
13104}
13105
13106fn form_status_chip(
13107 ui: &mut UiDocument,
13108 parent: UiNodeId,
13109 name: &'static str,
13110 label: &'static str,
13111 active: bool,
13112) {
13113 let chip = ui.add_child(
13114 parent,
13115 UiNode::container(
13116 name,
13117 LayoutStyle::new()
13118 .with_width(82.0)
13119 .with_height(24.0)
13120 .with_padding(4.0)
13121 .with_flex_shrink(0.0),
13122 )
13123 .with_visual(UiVisual::panel(
13124 if active {
13125 color(35, 74, 54)
13126 } else {
13127 color(28, 34, 43)
13128 },
13129 Some(StrokeStyle::new(
13130 if active {
13131 color(90, 160, 112)
13132 } else {
13133 color(60, 72, 88)
13134 },
13135 1.0,
13136 )),
13137 4.0,
13138 )),
13139 );
13140 widgets::label(
13141 ui,
13142 chip,
13143 format!("{name}.label"),
13144 label,
13145 text(11.0, color(218, 228, 240)),
13146 LayoutStyle::new()
13147 .with_width_percent(1.0)
13148 .with_height_percent(1.0),
13149 );
13150}
13151
13152fn profile_form_summary(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
13153 let has_errors = widgets::form_has_errors(&state.form);
13154 let title = profile_form_summary_title(state, has_errors);
13155 let detail = format!(
13156 "{} | {} | {}",
13157 profile_summary_value(state.form_name_text.text(), "No name"),
13158 profile_summary_value(state.form_email_text.text(), "No email"),
13159 profile_summary_value(state.form_role_text.text(), "No role"),
13160 );
13161 let hint = profile_form_summary_hint(state, has_errors);
13162 let stroke = if has_errors {
13163 color(196, 94, 104)
13164 } else if state.form.dirty {
13165 color(205, 160, 71)
13166 } else if state.form.submitted {
13167 color(91, 164, 119)
13168 } else {
13169 color(60, 72, 88)
13170 };
13171 let summary = ui.add_child(
13172 parent,
13173 UiNode::container(
13174 "forms.profile.summary",
13175 LayoutStyle::column()
13176 .with_width_percent(1.0)
13177 .with_padding(10.0)
13178 .with_gap(4.0)
13179 .with_flex_shrink(0.0),
13180 )
13181 .with_visual(UiVisual::panel(
13182 color(20, 25, 32),
13183 Some(StrokeStyle::new(stroke, 1.0)),
13184 4.0,
13185 ))
13186 .with_accessibility(
13187 AccessibilityMeta::new(AccessibilityRole::Group)
13188 .label("Live profile summary")
13189 .value(format!("{title}. {detail}. {hint}")),
13190 ),
13191 );
13192 widgets::label(
13193 ui,
13194 summary,
13195 "forms.profile.summary.title",
13196 title,
13197 text(13.0, color(232, 240, 250)),
13198 LayoutStyle::new().with_width_percent(1.0),
13199 );
13200 widgets::label(
13201 ui,
13202 summary,
13203 "forms.profile.summary.detail",
13204 detail,
13205 text(12.0, color(186, 198, 216)),
13206 LayoutStyle::new().with_width_percent(1.0),
13207 );
13208 widgets::label(
13209 ui,
13210 summary,
13211 "forms.profile.summary.hint",
13212 hint,
13213 text(11.0, color(154, 166, 184)),
13214 LayoutStyle::new().with_width_percent(1.0),
13215 );
13216}
13217
13218fn profile_form_summary_title(state: &ShowcaseState, has_errors: bool) -> &'static str {
13219 if has_errors {
13220 "Profile needs fixes"
13221 } else if state.form.submitted {
13222 "Profile submitted"
13223 } else if state.form.dirty {
13224 "Profile draft"
13225 } else {
13226 "Profile saved"
13227 }
13228}
13229
13230fn profile_form_summary_hint(state: &ShowcaseState, has_errors: bool) -> &'static str {
13231 if has_errors {
13232 "Fix validation errors before applying or submitting."
13233 } else if state.form.dirty {
13234 "Apply saves the draft; Submit saves and marks it submitted."
13235 } else if state.form.submitted {
13236 "Submission completed. Apply stays disabled until something changes."
13237 } else {
13238 "No pending changes. Submit marks the saved profile submitted."
13239 }
13240}
13241
13242fn profile_summary_value<'a>(value: &'a str, empty: &'static str) -> &'a str {
13243 let value = value.trim();
13244 if value.is_empty() {
13245 empty
13246 } else {
13247 value
13248 }
13249}
13250
13251#[allow(clippy::field_reassign_with_default)]
13252fn form_text_field(
13253 ui: &mut UiDocument,
13254 parent: UiNodeId,
13255 name: &'static str,
13256 input: &TextInputState,
13257 focused: FocusedTextInput,
13258 state: &ShowcaseState,
13259) {
13260 let mut options = TextInputOptions::default();
13261 options.layout = LayoutStyle::new().with_width_percent(1.0).with_height(30.0);
13262 options.text_style = text(12.0, color(230, 236, 246));
13263 options.placeholder_style = text(12.0, color(144, 156, 174));
13264 options.placeholder = "Required".to_string();
13265 options.edit_action = Some(format!("{name}.edit").into());
13266 options.focused = state.focused_text == Some(focused);
13267 options.caret_visible = caret_visible(state.caret_phase);
13268 widgets::text_input(ui, parent, name, input, options);
13269}
13270
13271fn profile_email_valid(email: &str) -> bool {
13272 let email = email.trim();
13273 let Some((local, domain)) = email.split_once('@') else {
13274 return false;
13275 };
13276 !local.is_empty() && domain.contains('.') && !domain.ends_with('.')
13277}
13278
13279fn drag_source_layout() -> LayoutStyle {
13280 LayoutStyle::row()
13281 .with_width(128.0)
13282 .with_height(40.0)
13283 .with_padding(8.0)
13284 .with_gap(6.0)
13285 .with_flex_shrink(0.0)
13286}
13287
13288fn drop_zone_layout() -> LayoutStyle {
13289 LayoutStyle::column()
13290 .with_width(128.0)
13291 .with_height(78.0)
13292 .with_padding(10.0)
13293 .with_gap(6.0)
13294 .with_flex_shrink(0.0)
13295}
13296
13297fn dnd_operation_chip(
13298 ui: &mut UiDocument,
13299 parent: UiNodeId,
13300 name: &'static str,
13301 label: &'static str,
13302) {
13303 let chip = ui.add_child(
13304 parent,
13305 UiNode::container(
13306 name,
13307 LayoutStyle::new()
13308 .with_width(58.0)
13309 .with_height(22.0)
13310 .with_padding(3.0)
13311 .with_flex_shrink(0.0),
13312 )
13313 .with_visual(UiVisual::panel(
13314 color(26, 32, 42),
13315 Some(StrokeStyle::new(color(62, 76, 94), 1.0)),
13316 3.0,
13317 )),
13318 );
13319 widgets::label(
13320 ui,
13321 chip,
13322 format!("{name}.label"),
13323 label,
13324 text(11.0, color(190, 204, 222)),
13325 LayoutStyle::new()
13326 .with_width_percent(1.0)
13327 .with_height_percent(1.0),
13328 );
13329}
13330
13331fn media_preview_image_layout() -> LayoutStyle {
13332 LayoutStyle::size(46.0, 46.0).with_flex_shrink(0.0)
13333}
13334
13335fn media_icon_columns(state: &ShowcaseState) -> usize {
13336 let theme = state.app_theme();
13337 let options = showcase_desktop_options(state.last_desktop_size, &theme);
13338 let window_width = state
13339 .desktop
13340 .size("media", default_window_size("media"))
13341 .width;
13342 let content_width = (window_width - options.content_padding * 2.0).max(MEDIA_ICON_TILE_WIDTH);
13343 let pitch = MEDIA_ICON_TILE_WIDTH + MEDIA_ICON_GRID_GAP;
13344 (((content_width + MEDIA_ICON_GRID_GAP) / pitch).floor() as usize).clamp(1, MEDIA_ICON_COLUMNS)
13345}
13346
13347fn media_icon_grid_width(columns: usize) -> f32 {
13348 let columns = columns.max(1);
13349 columns as f32 * MEDIA_ICON_TILE_WIDTH + columns.saturating_sub(1) as f32 * MEDIA_ICON_GRID_GAP
13350}
13351
13352fn media_icon_grid_height(columns: usize, item_count: usize) -> f32 {
13353 let columns = columns.max(1);
13354 let rows = item_count.div_ceil(columns).max(1);
13355 rows as f32 * MEDIA_ICON_TILE_HEIGHT + rows.saturating_sub(1) as f32 * MEDIA_ICON_GRID_GAP
13356}
13357
13358fn media_icon_grid(
13359 ui: &mut UiDocument,
13360 parent: UiNodeId,
13361 name: impl Into<String>,
13362 columns: usize,
13363 item_count: usize,
13364) -> UiNodeId {
13365 let columns = columns.clamp(1, MEDIA_ICON_COLUMNS);
13366 let rows = item_count.div_ceil(columns).max(1);
13367 let width = media_icon_grid_width(columns);
13368 let height = media_icon_grid_height(columns, item_count);
13369 let layout = operad::layout::with_grid_template_rows(
13370 operad::layout::with_grid_template_columns(
13371 Layout::grid()
13372 .size(LayoutSize::points(width, height))
13373 .gap(LayoutGap::points(MEDIA_ICON_GRID_GAP, MEDIA_ICON_GRID_GAP))
13374 .flex(0.0, 0.0, LayoutDimension::Auto)
13375 .to_layout_style(),
13376 (0..columns).map(|_| LayoutGridTrack::points(MEDIA_ICON_TILE_WIDTH)),
13377 ),
13378 (0..rows).map(|_| LayoutGridTrack::points(MEDIA_ICON_TILE_HEIGHT)),
13379 );
13380 ui.add_child(parent, UiNode::container(name, layout))
13381}
13382
13383fn media_icon_tile(ui: &mut UiDocument, parent: UiNodeId, icon: BuiltInIcon) {
13384 let name = icon.key().replace('.', "_").replace('-', "_");
13385 let tile = ui.add_child(
13386 parent,
13387 UiNode::container(
13388 format!("media.icon_tile.{name}"),
13389 LayoutStyle::column()
13390 .with_width(MEDIA_ICON_TILE_WIDTH)
13391 .with_height(MEDIA_ICON_TILE_HEIGHT)
13392 .with_padding(6.0)
13393 .with_gap(4.0)
13394 .with_flex_shrink(0.0),
13395 )
13396 .with_visual(UiVisual::panel(
13397 color(17, 22, 30),
13398 Some(StrokeStyle::new(color(50, 62, 78), 1.0)),
13399 4.0,
13400 )),
13401 );
13402 widgets::image(
13403 ui,
13404 tile,
13405 format!("media.icon.{name}"),
13406 icon_image(icon),
13407 widgets::ImageOptions::default()
13408 .with_layout(LayoutStyle::size(28.0, 28.0))
13409 .with_accessibility_label(icon.label()),
13410 );
13411 widgets::label(
13412 ui,
13413 tile,
13414 format!("media.icon_label.{name}"),
13415 icon.label(),
13416 text(9.0, color(180, 194, 214)),
13417 LayoutStyle::new().with_width_percent(1.0).with_height(30.0),
13418 );
13419}
13420
13421fn slider_checkbox(
13422 ui: &mut UiDocument,
13423 parent: UiNodeId,
13424 name: &'static str,
13425 label: &'static str,
13426 checked: bool,
13427) {
13428 slider_checkbox_with_layout(
13429 ui,
13430 parent,
13431 name,
13432 label,
13433 checked,
13434 LayoutStyle::new().with_width_percent(1.0).with_height(30.0),
13435 );
13436}
13437
13438fn slider_checkbox_with_layout(
13439 ui: &mut UiDocument,
13440 parent: UiNodeId,
13441 name: &'static str,
13442 label: &'static str,
13443 checked: bool,
13444 layout: LayoutStyle,
13445) {
13446 let mut options = widgets::CheckboxOptions::default().with_action(name);
13447 options.layout = layout;
13448 options.text_style = text(12.0, color(220, 228, 238));
13449 widgets::checkbox(ui, parent, name, label, checked, options);
13450}
13451
13452fn choice_button(
13453 ui: &mut UiDocument,
13454 parent: UiNodeId,
13455 name: &'static str,
13456 label: &'static str,
13457 selected: bool,
13458) {
13459 let mut options =
13460 widgets::ButtonOptions::new(LayoutStyle::new().with_width(78.0).with_height(28.0))
13461 .with_action(name);
13462 options.visual = if selected {
13463 button_visual(48, 112, 184)
13464 } else {
13465 button_visual(38, 46, 58)
13466 };
13467 options.hovered_visual = Some(button_visual(65, 86, 106));
13468 options.pressed_visual = Some(button_visual(34, 54, 84));
13469 options.text_style = text(12.0, color(238, 244, 252));
13470 widgets::button(ui, parent, name, label, options);
13471}
13472
13473fn divider(ui: &mut UiDocument, parent: UiNodeId, name: &'static str) {
13474 ui.add_child(
13475 parent,
13476 UiNode::container(
13477 name,
13478 LayoutStyle::new()
13479 .with_width_percent(1.0)
13480 .with_height(1.0)
13481 .with_flex_shrink(0.0),
13482 )
13483 .with_visual(UiVisual::panel(color(48, 58, 72), None, 0.0)),
13484 );
13485}
13486
13487fn canvas(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
13488 let canvas_intrinsic = UiSize::new(720.0, 405.0);
13489 let body = section_with_min_viewport(ui, parent, "canvas", "Canvas", UiSize::new(720.0, 458.0));
13490 let controls = wrapping_row(ui, body, "canvas.options", 10.0);
13491 canvas_option_checkbox(
13492 ui,
13493 controls,
13494 "canvas.grow_horizontal",
13495 "Grow width",
13496 state.canvas_grow_horizontal,
13497 );
13498 canvas_option_checkbox(
13499 ui,
13500 controls,
13501 "canvas.grow_vertical",
13502 "Grow height",
13503 state.canvas_grow_vertical,
13504 );
13505 canvas_option_checkbox(
13506 ui,
13507 controls,
13508 "canvas.keep_aspect_ratio",
13509 "Keep aspect ratio",
13510 state.canvas_keep_aspect_ratio,
13511 );
13512
13513 let mut options = widgets::CanvasOptions::default()
13514 .with_accessibility_label("Shader canvas")
13515 .with_action("canvas.rotate")
13516 .with_intrinsic_size(canvas_intrinsic);
13517 options.action_mode = WidgetActionMode::Drag;
13518 if state.canvas_keep_aspect_ratio {
13519 options = options.with_aspect_ratio(16.0 / 9.0);
13520 }
13521 let canvas_width = if state.canvas_grow_horizontal {
13522 LayoutDimension::percent(1.0)
13523 } else {
13524 LayoutDimension::points(canvas_intrinsic.width)
13525 };
13526 let canvas_height = if state.canvas_grow_vertical {
13527 LayoutDimension::percent(1.0)
13528 } else {
13529 LayoutDimension::points(canvas_intrinsic.height)
13530 };
13531 options.layout = Layout::new()
13532 .size(LayoutSize::new(canvas_width, canvas_height))
13533 .min_size(LayoutSize::points(
13534 canvas_intrinsic.width,
13535 canvas_intrinsic.height,
13536 ))
13537 .flex(
13538 if state.canvas_grow_vertical { 1.0 } else { 0.0 },
13539 1.0,
13540 LayoutDimension::Auto,
13541 )
13542 .to_layout_style();
13543 options.visual = UiVisual::panel(
13544 color(18, 22, 28),
13545 Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
13546 4.0,
13547 );
13548 widgets::canvas(
13549 ui,
13550 body,
13551 "canvas.shader",
13552 CanvasContent::new("canvas.shader").program(showcase_canvas_program(state.cube)),
13553 options,
13554 );
13555}
13556
13557fn canvas_option_checkbox(
13558 ui: &mut UiDocument,
13559 parent: UiNodeId,
13560 name: &'static str,
13561 label: &'static str,
13562 checked: bool,
13563) {
13564 let mut options = widgets::CheckboxOptions::default()
13565 .with_action(name)
13566 .with_text_style(text(12.0, color(220, 228, 238)));
13567 options.layout = LayoutStyle::new().with_height(28.0).with_flex_shrink(0.0);
13568 widgets::checkbox(ui, parent, name, label, checked, options);
13569}
13570
13571fn showcase_canvas_program(cube: CanvasCubeState) -> CanvasRenderProgram {
13572 CanvasRenderProgram::wgsl(include_str!("shaders/showcase_canvas.wgsl"))
13573 .label("showcase.canvas")
13574 .constant("CUBE_YAW", cube.yaw as f64)
13575 .constant("CUBE_PITCH", cube.pitch as f64)
13576 .clear_color(Some(color(18, 22, 28)))
13577}
13578
13579fn section(
13580 ui: &mut UiDocument,
13581 parent: UiNodeId,
13582 name: impl Into<String>,
13583 _title: impl Into<String>,
13584) -> UiNodeId {
13585 section_with_min_viewport(ui, parent, name, _title, UiSize::ZERO)
13586}
13587
13588fn section_with_min_viewport(
13589 ui: &mut UiDocument,
13590 parent: UiNodeId,
13591 name: impl Into<String>,
13592 _title: impl Into<String>,
13593 min_viewport_size: UiSize,
13594) -> UiNodeId {
13595 let name = name.into();
13596 let layout = Layout::column()
13597 .size(LayoutSize::percent(1.0, 1.0))
13598 .min_size(LayoutSize::points(
13599 min_viewport_size.width.max(0.0),
13600 min_viewport_size.height.max(0.0),
13601 ))
13602 .gap(LayoutGap::points(10.0, 10.0))
13603 .flex(1.0, 1.0, LayoutDimension::Auto)
13604 .to_layout_style();
13605 widgets::scroll_area(
13606 ui,
13607 parent,
13608 format!("{name}.section_scroll"),
13609 ScrollAxes::BOTH,
13610 layout,
13611 )
13612}
13613
13614fn row(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>, gap: f32) -> UiNodeId {
13615 ui.add_child(
13616 parent,
13617 UiNode::container(
13618 name,
13619 Layout::row()
13620 .size(LayoutSize::new(
13621 LayoutDimension::percent(1.0),
13622 LayoutDimension::Auto,
13623 ))
13624 .align_items(LayoutAlignment::Center)
13625 .gap(LayoutGap::points(gap, gap))
13626 .flex(0.0, 0.0, LayoutDimension::Auto)
13627 .to_layout_style(),
13628 ),
13629 )
13630}
13631
13632fn wrapping_row(
13633 ui: &mut UiDocument,
13634 parent: UiNodeId,
13635 name: impl Into<String>,
13636 gap: f32,
13637) -> UiNodeId {
13638 ui.add_child(
13639 parent,
13640 UiNode::container(
13641 name,
13642 Layout::row()
13643 .size(LayoutSize::new(
13644 LayoutDimension::percent(1.0),
13645 LayoutDimension::Auto,
13646 ))
13647 .min_size(LayoutSize::points(0.0, 0.0))
13648 .align_items(LayoutAlignment::Center)
13649 .gap(LayoutGap::points(gap, gap))
13650 .flex_wrap(LayoutFlexWrap::Wrap)
13651 .flex(0.0, 0.0, LayoutDimension::Auto)
13652 .to_layout_style(),
13653 ),
13654 )
13655}Sourcepub const fn percent(width: f32, height: f32) -> Self
pub const fn percent(width: f32, height: f32) -> Self
Examples found in repository?
examples/showcase.rs (line 12484)
12470fn styling_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12471 let preview_scene_size = style_preview_scene_size(state.styling);
12472 let preview_min_width = preview_scene_size.width + 16.0;
12473 let preview_min_height = preview_scene_size.height + 16.0;
12474 let body_min_width = STYLING_CONTROLS_WIDTH + 1.0 + preview_min_width + 20.0;
12475 let body = section_with_min_viewport(
12476 ui,
12477 parent,
12478 "styling",
12479 "Styling",
12480 UiSize::new(body_min_width, preview_min_height),
12481 );
12482 let grid_layout = operad::layout::with_grid_template_columns(
12483 Layout::grid()
12484 .size(LayoutSize::percent(1.0, 1.0))
12485 .gap(LayoutGap::points(10.0, 10.0))
12486 .to_layout_style(),
12487 [
12488 LayoutGridTrack::points(STYLING_CONTROLS_WIDTH),
12489 LayoutGridTrack::points(1.0),
12490 LayoutGridTrack::minmax_points_fraction(preview_min_width, 1.0),
12491 ],
12492 );
12493 let grid = ui.add_child(body, UiNode::container("styling.grid", grid_layout));
12494 let controls = ui.add_child(
12495 grid,
12496 UiNode::container(
12497 "styling.controls",
12498 LayoutStyle::column()
12499 .with_width(STYLING_CONTROLS_WIDTH)
12500 .with_height_percent(1.0)
12501 .with_flex_shrink(0.0)
12502 .gap(6.0),
12503 ),
12504 );
12505 style_edge_group(
12506 ui,
12507 controls,
12508 "styling.inner",
12509 "Inner margin",
12510 "styling.inner_same",
12511 state.styling.inner_same,
12512 [
12513 ("Left", "styling.inner", state.styling.inner_margin),
12514 ("Right", "styling.inner_right", state.styling.inner_right),
12515 ("Top", "styling.inner_top", state.styling.inner_top),
12516 ("Bottom", "styling.inner_bottom", state.styling.inner_bottom),
12517 ],
12518 0.0..32.0,
12519 );
12520 style_edge_group(
12521 ui,
12522 controls,
12523 "styling.outer",
12524 "Outer margin",
12525 "styling.outer_same",
12526 state.styling.outer_same,
12527 [
12528 ("Left", "styling.outer", state.styling.outer_margin),
12529 ("Right", "styling.outer_right", state.styling.outer_right),
12530 ("Top", "styling.outer_top", state.styling.outer_top),
12531 ("Bottom", "styling.outer_bottom", state.styling.outer_bottom),
12532 ],
12533 0.0..40.0,
12534 );
12535 style_edge_group(
12536 ui,
12537 controls,
12538 "styling.radius",
12539 "Corner radius",
12540 "styling.radius_same",
12541 state.styling.radius_same,
12542 [
12543 ("NW", "styling.radius", state.styling.corner_radius),
12544 ("NE", "styling.radius_ne", state.styling.corner_ne),
12545 ("SW", "styling.radius_sw", state.styling.corner_sw),
12546 ("SE", "styling.radius_se", state.styling.corner_se),
12547 ],
12548 0.0..28.0,
12549 );
12550 style_fill_group(ui, controls, state);
12551 style_stroke_group(ui, controls, state);
12552 style_shadow_group(ui, controls, state);
12553 widgets::separator(
12554 ui,
12555 grid,
12556 "styling.preview.separator",
12557 widgets::SeparatorOptions::vertical().with_layout(
12558 LayoutStyle::new()
12559 .with_width(1.0)
12560 .with_height_percent(1.0)
12561 .with_flex_shrink(0.0),
12562 ),
12563 );
12564
12565 let preview = ui.add_child(
12566 grid,
12567 UiNode::container(
12568 "styling.preview",
12569 operad::layout::with_min_size(
12570 LayoutStyle::column()
12571 .with_width_percent(1.0)
12572 .with_height_percent(1.0)
12573 .with_flex_shrink(0.0)
12574 .padding(8.0),
12575 operad::layout::px(preview_min_width),
12576 operad::layout::px(preview_min_height),
12577 ),
12578 )
12579 .with_visual(UiVisual::panel(color(17, 20, 25), None, 0.0)),
12580 );
12581 style_preview(ui, preview, state.styling);
12582}
12583
12584#[allow(clippy::too_many_arguments)]
12585fn style_edge_group(
12586 ui: &mut UiDocument,
12587 parent: UiNodeId,
12588 name: &'static str,
12589 title: &'static str,
12590 same_action: &'static str,
12591 same: bool,
12592 values: [(&'static str, &'static str, f32); 4],
12593 range: std::ops::Range<f32>,
12594) {
12595 let group = style_control_group(ui, parent, format!("{name}.group"));
12596 style_group_title(ui, group, format!("{name}.title"), title);
12597 let fields = ui.add_child(
12598 group,
12599 UiNode::container(
12600 format!("{name}.fields"),
12601 LayoutStyle::column()
12602 .with_width(138.0)
12603 .with_flex_shrink(0.0)
12604 .gap(3.0),
12605 ),
12606 );
12607 style_compact_checkbox(ui, fields, same_action, "same", same);
12608 if same {
12609 style_number_row(ui, fields, values[0].1, "All", values[0].2, range, 0);
12610 } else {
12611 for (label, action, value) in values {
12612 style_number_row(ui, fields, action, label, value, range.clone(), 0);
12613 }
12614 }
12615}
12616
12617fn style_fill_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12618 let group = style_control_group(ui, parent, "styling.fill.group");
12619 style_group_title(ui, group, "styling.fill.title", "Fill");
12620 let fields = style_group_fields(
12621 ui,
12622 group,
12623 "styling.fill.fields",
12624 STYLING_WIDE_FIELDS_WIDTH,
12625 4.0,
12626 );
12627 style_color_button_row(
12628 ui,
12629 fields,
12630 "styling.fill_color_button",
12631 "",
12632 state.styling.fill_color(),
12633 "Pick fill color",
12634 );
12635 if state.styling_fill_picker_open {
12636 ext_widgets::color_picker(
12637 ui,
12638 fields,
12639 "styling.fill_picker",
12640 &state.styling_fill_picker,
12641 ext_widgets::ColorPickerOptions::default()
12642 .with_label("Fill")
12643 .with_action_prefix("styling.fill_picker"),
12644 );
12645 }
12646}
12647
12648fn style_stroke_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12649 let group = style_control_group(ui, parent, "styling.stroke.group");
12650 style_group_title(ui, group, "styling.stroke.title", "Stroke");
12651 let fields = style_group_fields(
12652 ui,
12653 group,
12654 "styling.stroke.fields",
12655 STYLING_WIDE_FIELDS_WIDTH,
12656 4.0,
12657 );
12658 let width_row = row(ui, fields, "styling.stroke.row", 6.0);
12659 style_inline_number(
12660 ui,
12661 width_row,
12662 "styling.stroke",
12663 "width",
12664 state.styling.stroke_width,
12665 0.0..STYLING_STROKE_MAX,
12666 1,
12667 );
12668 let mut options = widgets::SliderOptions::default()
12669 .with_layout(
12670 LayoutStyle::new()
12671 .with_width(60.0)
12672 .with_height(20.0)
12673 .with_flex_shrink(0.0),
12674 )
12675 .with_value_edit_action("styling.stroke");
12676 options.fill_color = color(120, 170, 230);
12677 widgets::slider(
12678 ui,
12679 width_row,
12680 "styling.stroke.slider",
12681 (state.styling.stroke_width / STYLING_STROKE_MAX).clamp(0.0, 1.0),
12682 0.0..1.0,
12683 options,
12684 );
12685 style_color_button_row(
12686 ui,
12687 fields,
12688 "styling.stroke_color_button",
12689 "",
12690 state.styling.stroke_color(),
12691 "Pick stroke color",
12692 );
12693 if state.styling_stroke_picker_open {
12694 ext_widgets::color_picker(
12695 ui,
12696 fields,
12697 "styling.stroke_picker",
12698 &state.styling_stroke_picker,
12699 ext_widgets::ColorPickerOptions::default()
12700 .with_label("Stroke color")
12701 .with_action_prefix("styling.stroke_picker"),
12702 );
12703 }
12704}
12705
12706fn style_shadow_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
12707 let group = style_control_group(ui, parent, "styling.shadow.group");
12708 style_group_title(ui, group, "styling.shadow.title", "Shadow");
12709 let fields = style_group_fields(
12710 ui,
12711 group,
12712 "styling.shadow.fields",
12713 STYLING_WIDE_FIELDS_WIDTH,
12714 4.0,
12715 );
12716 let offsets = row(ui, fields, "styling.shadow.offsets", 6.0);
12717 style_inline_number(
12718 ui,
12719 offsets,
12720 "styling.shadow_x",
12721 "x",
12722 state.styling.shadow_x,
12723 -24.0..24.0,
12724 0,
12725 );
12726 style_inline_number(
12727 ui,
12728 offsets,
12729 "styling.shadow_y",
12730 "y",
12731 state.styling.shadow_y,
12732 -24.0..24.0,
12733 0,
12734 );
12735 let spread = row(ui, fields, "styling.shadow.blur_spread", 6.0);
12736 style_inline_number(
12737 ui,
12738 spread,
12739 "styling.shadow",
12740 "blur",
12741 state.styling.shadow_blur,
12742 0.0..32.0,
12743 0,
12744 );
12745 style_inline_number(
12746 ui,
12747 spread,
12748 "styling.shadow_spread",
12749 "spread",
12750 state.styling.shadow_spread,
12751 0.0..16.0,
12752 0,
12753 );
12754 style_color_button_row(
12755 ui,
12756 fields,
12757 "styling.shadow_color_button",
12758 "",
12759 state.styling.shadow_color(),
12760 "Pick shadow color",
12761 );
12762 if state.styling_shadow_picker_open {
12763 ext_widgets::color_picker(
12764 ui,
12765 fields,
12766 "styling.shadow_picker",
12767 &state.styling_shadow_picker,
12768 ext_widgets::ColorPickerOptions::default()
12769 .with_label("Shadow color")
12770 .with_action_prefix("styling.shadow_picker"),
12771 );
12772 }
12773}
12774
12775fn style_control_group(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>) -> UiNodeId {
12776 ui.add_child(
12777 parent,
12778 UiNode::container(
12779 name,
12780 LayoutStyle::row()
12781 .with_width_percent(1.0)
12782 .with_flex_shrink(0.0)
12783 .padding(4.0)
12784 .gap(8.0),
12785 )
12786 .with_visual(UiVisual::panel(color(23, 27, 33), None, 2.0)),
12787 )
12788}
12789
12790fn style_group_fields(
12791 ui: &mut UiDocument,
12792 parent: UiNodeId,
12793 name: impl Into<String>,
12794 width: f32,
12795 gap: f32,
12796) -> UiNodeId {
12797 ui.add_child(
12798 parent,
12799 UiNode::container(
12800 name,
12801 LayoutStyle::column()
12802 .with_width(width)
12803 .with_flex_shrink(0.0)
12804 .gap(gap),
12805 ),
12806 )
12807}
12808
12809fn style_group_title(
12810 ui: &mut UiDocument,
12811 parent: UiNodeId,
12812 name: impl Into<String>,
12813 label: &'static str,
12814) {
12815 widgets::label(
12816 ui,
12817 parent,
12818 name,
12819 label,
12820 text(12.0, color(166, 176, 190)),
12821 LayoutStyle::new()
12822 .with_width(88.0)
12823 .with_flex_shrink(0.0)
12824 .with_height(22.0),
12825 );
12826}
12827
12828fn style_color_button_row(
12829 ui: &mut UiDocument,
12830 parent: UiNodeId,
12831 action: &'static str,
12832 label: &'static str,
12833 value: ColorRgba,
12834 accessibility_label: &'static str,
12835) {
12836 let row = row(ui, parent, format!("{action}.row"), 8.0);
12837 if !label.is_empty() {
12838 widgets::label(
12839 ui,
12840 row,
12841 format!("{action}.label"),
12842 label,
12843 text(12.0, color(166, 176, 190)),
12844 LayoutStyle::new()
12845 .with_width(86.0)
12846 .with_flex_shrink(0.0)
12847 .with_height(24.0),
12848 );
12849 }
12850 ext_widgets::color_edit_button(
12851 ui,
12852 row,
12853 action,
12854 value,
12855 color_mini_button_options(action)
12856 .with_format(ext_widgets::ColorValueFormat::Rgba)
12857 .accessibility_label(accessibility_label),
12858 );
12859 widgets::label(
12860 ui,
12861 row,
12862 format!("{action}.value"),
12863 ext_widgets::color_picker::format_hex_color(value, value.a < 255),
12864 text(12.0, color(226, 232, 242)),
12865 LayoutStyle::new().with_width(96.0).with_height(24.0),
12866 );
12867}
12868
12869fn style_number_row(
12870 ui: &mut UiDocument,
12871 parent: UiNodeId,
12872 name: &'static str,
12873 label: &'static str,
12874 value: f32,
12875 range: std::ops::Range<f32>,
12876 decimals: u8,
12877) {
12878 let row = row(ui, parent, format!("{name}.row"), 6.0);
12879 widgets::label(
12880 ui,
12881 row,
12882 format!("{name}.label"),
12883 label,
12884 text(12.0, color(166, 176, 190)),
12885 LayoutStyle::new().with_width(48.0).with_height(22.0),
12886 );
12887 style_value_input(ui, row, name, value, range, decimals);
12888}
12889
12890fn style_inline_number(
12891 ui: &mut UiDocument,
12892 parent: UiNodeId,
12893 name: &'static str,
12894 label: &'static str,
12895 value: f32,
12896 range: std::ops::Range<f32>,
12897 decimals: u8,
12898) {
12899 let row = compact_row(ui, parent, format!("{name}.inline"), 3.0);
12900 widgets::label(
12901 ui,
12902 row,
12903 format!("{name}.inline_label"),
12904 format!("{label}:"),
12905 text(12.0, color(166, 176, 190)),
12906 LayoutStyle::new()
12907 .with_width(if label.len() > 1 { 42.0 } else { 16.0 })
12908 .with_height(22.0),
12909 );
12910 style_value_input(ui, row, name, value, range, decimals);
12911}
12912
12913fn style_value_input(
12914 ui: &mut UiDocument,
12915 parent: UiNodeId,
12916 name: &'static str,
12917 value: f32,
12918 range: std::ops::Range<f32>,
12919 decimals: u8,
12920) {
12921 let mut options = widgets::DragValueOptions::default()
12922 .with_layout(
12923 LayoutStyle::row()
12924 .with_width(STYLING_VALUE_INPUT_WIDTH)
12925 .with_height(22.0)
12926 .with_flex_shrink(0.0)
12927 .with_align_items(taffy::prelude::AlignItems::Center)
12928 .with_justify_content(taffy::prelude::JustifyContent::Center)
12929 .with_padding(4.0),
12930 )
12931 .with_range(ext_widgets::NumericRange::new(
12932 f64::from(range.start),
12933 f64::from(range.end),
12934 ))
12935 .with_precision(ext_widgets::NumericPrecision::decimals(decimals))
12936 .with_action(name);
12937 options.text_style = text(12.0, color(226, 232, 242));
12938 widgets::drag_value_input(ui, parent, name, f64::from(value), options);
12939}
12940
12941fn style_compact_checkbox(
12942 ui: &mut UiDocument,
12943 parent: UiNodeId,
12944 name: &'static str,
12945 label: &'static str,
12946 checked: bool,
12947) {
12948 let mut options = widgets::CheckboxOptions::default().with_action(name);
12949 options.layout = LayoutStyle::new().with_width(92.0).with_height(22.0);
12950 options.text_style = text(12.0, color(220, 228, 238));
12951 widgets::checkbox(ui, parent, name, label, checked, options);
12952}
12953
12954fn compact_row(
12955 ui: &mut UiDocument,
12956 parent: UiNodeId,
12957 name: impl Into<String>,
12958 gap: f32,
12959) -> UiNodeId {
12960 ui.add_child(
12961 parent,
12962 UiNode::container(
12963 name,
12964 LayoutStyle::row()
12965 .with_height(22.0)
12966 .with_flex_shrink(0.0)
12967 .with_align_items(taffy::prelude::AlignItems::Center)
12968 .gap(gap),
12969 ),
12970 )
12971}
12972
12973fn color_mini_button_options(action: &'static str) -> ext_widgets::ColorButtonOptions {
12974 ext_widgets::ColorButtonOptions::default()
12975 .with_layout(LayoutStyle::size(28.0, 24.0).with_flex_shrink(0.0))
12976 .with_swatch_size(UiSize::new(22.0, 18.0))
12977 .with_action(action)
12978 .show_label(false)
12979}
12980
12981fn style_preview(ui: &mut UiDocument, parent: UiNodeId, styling: StylingState) {
12982 let (frame, text_rect) = style_preview_rects(styling);
12983 let scene_size = style_preview_scene_size(styling);
12984 ui.add_child(
12985 parent,
12986 UiNode::scene(
12987 "styling.preview.scene",
12988 vec![
12989 ScenePrimitive::Rect(
12990 PaintRect::solid(frame, styling.fill_color())
12991 .stroke(AlignedStroke::inside(StrokeStyle::new(
12992 styling.stroke_color(),
12993 styling.stroke_width,
12994 )))
12995 .corner_radii(styling.radii())
12996 .effect(PaintEffect::shadow(
12997 styling.shadow_color(),
12998 UiPoint::new(styling.shadow_x, styling.shadow_y),
12999 styling.shadow_blur,
13000 styling.shadow_spread,
13001 )),
13002 ),
13003 ScenePrimitive::Text(
13004 PaintText::new("Content", text_rect, text(13.0, color(255, 255, 255)))
13005 .horizontal_align(TextHorizontalAlign::Center)
13006 .vertical_align(TextVerticalAlign::Center)
13007 .multiline(false),
13008 ),
13009 ],
13010 operad::layout::with_min_size(
13011 LayoutStyle::new()
13012 .with_width_percent(1.0)
13013 .with_height(180.0)
13014 .with_flex_shrink(0.0),
13015 operad::layout::px(scene_size.width),
13016 operad::layout::px(scene_size.height),
13017 ),
13018 ),
13019 );
13020}
13021
13022fn style_preview_rects(styling: StylingState) -> (UiRect, UiRect) {
13023 let outer = styling.outer_edges();
13024 let inner = styling.inner_edges();
13025 let frame = UiRect::new(
13026 22.0 + outer[0],
13027 28.0 + outer[2],
13028 108.0 + inner[0] + inner[1],
13029 40.0 + inner[2] + inner[3],
13030 );
13031 let text_rect = UiRect::new(
13032 frame.x + inner[0],
13033 frame.y + inner[2],
13034 (frame.width - inner[0] - inner[1]).max(1.0),
13035 (frame.height - inner[2] - inner[3]).max(1.0),
13036 );
13037 (frame, text_rect)
13038}
13039
13040fn style_preview_scene_size(styling: StylingState) -> UiSize {
13041 let (frame, text_rect) = style_preview_rects(styling);
13042 let shadow_outset = styling.shadow_blur.max(0.0) + styling.shadow_spread.max(0.0);
13043 let shadow_bounds = UiRect::new(
13044 frame.x + styling.shadow_x - shadow_outset,
13045 frame.y + styling.shadow_y - shadow_outset,
13046 frame.width + shadow_outset * 2.0,
13047 frame.height + shadow_outset * 2.0,
13048 );
13049 let right = frame
13050 .right()
13051 .max(text_rect.right())
13052 .max(shadow_bounds.right());
13053 let bottom = frame
13054 .bottom()
13055 .max(text_rect.bottom())
13056 .max(shadow_bounds.bottom())
13057 .max(180.0);
13058 UiSize::new(right.ceil().max(1.0), bottom.ceil().max(1.0))
13059}
13060
13061fn slider_options(state: &ShowcaseState, width: f32) -> widgets::SliderOptions {
13062 let mut options = widgets::SliderOptions::default().with_layout(
13063 LayoutStyle::new()
13064 .with_width(width)
13065 .with_height(24.0)
13066 .with_flex_shrink(0.0),
13067 );
13068 options.fill_color = if state.slider_trailing_color {
13069 state.slider_trailing_picker.value()
13070 } else {
13071 color(42, 49, 58)
13072 };
13073 options.thumb_shape = match state.slider_thumb_shape {
13074 SliderThumbChoice::Circle => widgets::slider::SliderThumbShape::Circle,
13075 SliderThumbChoice::Square => widgets::slider::SliderThumbShape::Square,
13076 SliderThumbChoice::Rectangle => widgets::slider::SliderThumbShape::Rectangle,
13077 };
13078 options.thumb_visual = UiVisual::panel(
13079 state.slider_thumb_picker.value(),
13080 Some(StrokeStyle::new(color(79, 93, 113), 1.0)),
13081 6.0,
13082 );
13083 options
13084}
13085
13086#[allow(clippy::field_reassign_with_default)]
13087fn slider_number_input(
13088 ui: &mut UiDocument,
13089 parent: UiNodeId,
13090 name: &'static str,
13091 input: &TextInputState,
13092 focused: FocusedTextInput,
13093 state: &ShowcaseState,
13094 width: f32,
13095) {
13096 let mut options = TextInputOptions::default();
13097 options.layout = LayoutStyle::new().with_width(width).with_height(28.0);
13098 options.text_style = text(12.0, color(230, 236, 246));
13099 options.placeholder_style = text(12.0, color(144, 156, 174));
13100 options.edit_action = Some(format!("{name}.edit").into());
13101 options.focused = state.focused_text == Some(focused);
13102 options.caret_visible = caret_visible(state.caret_phase);
13103 widgets::text_input(ui, parent, name, input, options);
13104}
13105
13106fn form_status_chip(
13107 ui: &mut UiDocument,
13108 parent: UiNodeId,
13109 name: &'static str,
13110 label: &'static str,
13111 active: bool,
13112) {
13113 let chip = ui.add_child(
13114 parent,
13115 UiNode::container(
13116 name,
13117 LayoutStyle::new()
13118 .with_width(82.0)
13119 .with_height(24.0)
13120 .with_padding(4.0)
13121 .with_flex_shrink(0.0),
13122 )
13123 .with_visual(UiVisual::panel(
13124 if active {
13125 color(35, 74, 54)
13126 } else {
13127 color(28, 34, 43)
13128 },
13129 Some(StrokeStyle::new(
13130 if active {
13131 color(90, 160, 112)
13132 } else {
13133 color(60, 72, 88)
13134 },
13135 1.0,
13136 )),
13137 4.0,
13138 )),
13139 );
13140 widgets::label(
13141 ui,
13142 chip,
13143 format!("{name}.label"),
13144 label,
13145 text(11.0, color(218, 228, 240)),
13146 LayoutStyle::new()
13147 .with_width_percent(1.0)
13148 .with_height_percent(1.0),
13149 );
13150}
13151
13152fn profile_form_summary(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
13153 let has_errors = widgets::form_has_errors(&state.form);
13154 let title = profile_form_summary_title(state, has_errors);
13155 let detail = format!(
13156 "{} | {} | {}",
13157 profile_summary_value(state.form_name_text.text(), "No name"),
13158 profile_summary_value(state.form_email_text.text(), "No email"),
13159 profile_summary_value(state.form_role_text.text(), "No role"),
13160 );
13161 let hint = profile_form_summary_hint(state, has_errors);
13162 let stroke = if has_errors {
13163 color(196, 94, 104)
13164 } else if state.form.dirty {
13165 color(205, 160, 71)
13166 } else if state.form.submitted {
13167 color(91, 164, 119)
13168 } else {
13169 color(60, 72, 88)
13170 };
13171 let summary = ui.add_child(
13172 parent,
13173 UiNode::container(
13174 "forms.profile.summary",
13175 LayoutStyle::column()
13176 .with_width_percent(1.0)
13177 .with_padding(10.0)
13178 .with_gap(4.0)
13179 .with_flex_shrink(0.0),
13180 )
13181 .with_visual(UiVisual::panel(
13182 color(20, 25, 32),
13183 Some(StrokeStyle::new(stroke, 1.0)),
13184 4.0,
13185 ))
13186 .with_accessibility(
13187 AccessibilityMeta::new(AccessibilityRole::Group)
13188 .label("Live profile summary")
13189 .value(format!("{title}. {detail}. {hint}")),
13190 ),
13191 );
13192 widgets::label(
13193 ui,
13194 summary,
13195 "forms.profile.summary.title",
13196 title,
13197 text(13.0, color(232, 240, 250)),
13198 LayoutStyle::new().with_width_percent(1.0),
13199 );
13200 widgets::label(
13201 ui,
13202 summary,
13203 "forms.profile.summary.detail",
13204 detail,
13205 text(12.0, color(186, 198, 216)),
13206 LayoutStyle::new().with_width_percent(1.0),
13207 );
13208 widgets::label(
13209 ui,
13210 summary,
13211 "forms.profile.summary.hint",
13212 hint,
13213 text(11.0, color(154, 166, 184)),
13214 LayoutStyle::new().with_width_percent(1.0),
13215 );
13216}
13217
13218fn profile_form_summary_title(state: &ShowcaseState, has_errors: bool) -> &'static str {
13219 if has_errors {
13220 "Profile needs fixes"
13221 } else if state.form.submitted {
13222 "Profile submitted"
13223 } else if state.form.dirty {
13224 "Profile draft"
13225 } else {
13226 "Profile saved"
13227 }
13228}
13229
13230fn profile_form_summary_hint(state: &ShowcaseState, has_errors: bool) -> &'static str {
13231 if has_errors {
13232 "Fix validation errors before applying or submitting."
13233 } else if state.form.dirty {
13234 "Apply saves the draft; Submit saves and marks it submitted."
13235 } else if state.form.submitted {
13236 "Submission completed. Apply stays disabled until something changes."
13237 } else {
13238 "No pending changes. Submit marks the saved profile submitted."
13239 }
13240}
13241
13242fn profile_summary_value<'a>(value: &'a str, empty: &'static str) -> &'a str {
13243 let value = value.trim();
13244 if value.is_empty() {
13245 empty
13246 } else {
13247 value
13248 }
13249}
13250
13251#[allow(clippy::field_reassign_with_default)]
13252fn form_text_field(
13253 ui: &mut UiDocument,
13254 parent: UiNodeId,
13255 name: &'static str,
13256 input: &TextInputState,
13257 focused: FocusedTextInput,
13258 state: &ShowcaseState,
13259) {
13260 let mut options = TextInputOptions::default();
13261 options.layout = LayoutStyle::new().with_width_percent(1.0).with_height(30.0);
13262 options.text_style = text(12.0, color(230, 236, 246));
13263 options.placeholder_style = text(12.0, color(144, 156, 174));
13264 options.placeholder = "Required".to_string();
13265 options.edit_action = Some(format!("{name}.edit").into());
13266 options.focused = state.focused_text == Some(focused);
13267 options.caret_visible = caret_visible(state.caret_phase);
13268 widgets::text_input(ui, parent, name, input, options);
13269}
13270
13271fn profile_email_valid(email: &str) -> bool {
13272 let email = email.trim();
13273 let Some((local, domain)) = email.split_once('@') else {
13274 return false;
13275 };
13276 !local.is_empty() && domain.contains('.') && !domain.ends_with('.')
13277}
13278
13279fn drag_source_layout() -> LayoutStyle {
13280 LayoutStyle::row()
13281 .with_width(128.0)
13282 .with_height(40.0)
13283 .with_padding(8.0)
13284 .with_gap(6.0)
13285 .with_flex_shrink(0.0)
13286}
13287
13288fn drop_zone_layout() -> LayoutStyle {
13289 LayoutStyle::column()
13290 .with_width(128.0)
13291 .with_height(78.0)
13292 .with_padding(10.0)
13293 .with_gap(6.0)
13294 .with_flex_shrink(0.0)
13295}
13296
13297fn dnd_operation_chip(
13298 ui: &mut UiDocument,
13299 parent: UiNodeId,
13300 name: &'static str,
13301 label: &'static str,
13302) {
13303 let chip = ui.add_child(
13304 parent,
13305 UiNode::container(
13306 name,
13307 LayoutStyle::new()
13308 .with_width(58.0)
13309 .with_height(22.0)
13310 .with_padding(3.0)
13311 .with_flex_shrink(0.0),
13312 )
13313 .with_visual(UiVisual::panel(
13314 color(26, 32, 42),
13315 Some(StrokeStyle::new(color(62, 76, 94), 1.0)),
13316 3.0,
13317 )),
13318 );
13319 widgets::label(
13320 ui,
13321 chip,
13322 format!("{name}.label"),
13323 label,
13324 text(11.0, color(190, 204, 222)),
13325 LayoutStyle::new()
13326 .with_width_percent(1.0)
13327 .with_height_percent(1.0),
13328 );
13329}
13330
13331fn media_preview_image_layout() -> LayoutStyle {
13332 LayoutStyle::size(46.0, 46.0).with_flex_shrink(0.0)
13333}
13334
13335fn media_icon_columns(state: &ShowcaseState) -> usize {
13336 let theme = state.app_theme();
13337 let options = showcase_desktop_options(state.last_desktop_size, &theme);
13338 let window_width = state
13339 .desktop
13340 .size("media", default_window_size("media"))
13341 .width;
13342 let content_width = (window_width - options.content_padding * 2.0).max(MEDIA_ICON_TILE_WIDTH);
13343 let pitch = MEDIA_ICON_TILE_WIDTH + MEDIA_ICON_GRID_GAP;
13344 (((content_width + MEDIA_ICON_GRID_GAP) / pitch).floor() as usize).clamp(1, MEDIA_ICON_COLUMNS)
13345}
13346
13347fn media_icon_grid_width(columns: usize) -> f32 {
13348 let columns = columns.max(1);
13349 columns as f32 * MEDIA_ICON_TILE_WIDTH + columns.saturating_sub(1) as f32 * MEDIA_ICON_GRID_GAP
13350}
13351
13352fn media_icon_grid_height(columns: usize, item_count: usize) -> f32 {
13353 let columns = columns.max(1);
13354 let rows = item_count.div_ceil(columns).max(1);
13355 rows as f32 * MEDIA_ICON_TILE_HEIGHT + rows.saturating_sub(1) as f32 * MEDIA_ICON_GRID_GAP
13356}
13357
13358fn media_icon_grid(
13359 ui: &mut UiDocument,
13360 parent: UiNodeId,
13361 name: impl Into<String>,
13362 columns: usize,
13363 item_count: usize,
13364) -> UiNodeId {
13365 let columns = columns.clamp(1, MEDIA_ICON_COLUMNS);
13366 let rows = item_count.div_ceil(columns).max(1);
13367 let width = media_icon_grid_width(columns);
13368 let height = media_icon_grid_height(columns, item_count);
13369 let layout = operad::layout::with_grid_template_rows(
13370 operad::layout::with_grid_template_columns(
13371 Layout::grid()
13372 .size(LayoutSize::points(width, height))
13373 .gap(LayoutGap::points(MEDIA_ICON_GRID_GAP, MEDIA_ICON_GRID_GAP))
13374 .flex(0.0, 0.0, LayoutDimension::Auto)
13375 .to_layout_style(),
13376 (0..columns).map(|_| LayoutGridTrack::points(MEDIA_ICON_TILE_WIDTH)),
13377 ),
13378 (0..rows).map(|_| LayoutGridTrack::points(MEDIA_ICON_TILE_HEIGHT)),
13379 );
13380 ui.add_child(parent, UiNode::container(name, layout))
13381}
13382
13383fn media_icon_tile(ui: &mut UiDocument, parent: UiNodeId, icon: BuiltInIcon) {
13384 let name = icon.key().replace('.', "_").replace('-', "_");
13385 let tile = ui.add_child(
13386 parent,
13387 UiNode::container(
13388 format!("media.icon_tile.{name}"),
13389 LayoutStyle::column()
13390 .with_width(MEDIA_ICON_TILE_WIDTH)
13391 .with_height(MEDIA_ICON_TILE_HEIGHT)
13392 .with_padding(6.0)
13393 .with_gap(4.0)
13394 .with_flex_shrink(0.0),
13395 )
13396 .with_visual(UiVisual::panel(
13397 color(17, 22, 30),
13398 Some(StrokeStyle::new(color(50, 62, 78), 1.0)),
13399 4.0,
13400 )),
13401 );
13402 widgets::image(
13403 ui,
13404 tile,
13405 format!("media.icon.{name}"),
13406 icon_image(icon),
13407 widgets::ImageOptions::default()
13408 .with_layout(LayoutStyle::size(28.0, 28.0))
13409 .with_accessibility_label(icon.label()),
13410 );
13411 widgets::label(
13412 ui,
13413 tile,
13414 format!("media.icon_label.{name}"),
13415 icon.label(),
13416 text(9.0, color(180, 194, 214)),
13417 LayoutStyle::new().with_width_percent(1.0).with_height(30.0),
13418 );
13419}
13420
13421fn slider_checkbox(
13422 ui: &mut UiDocument,
13423 parent: UiNodeId,
13424 name: &'static str,
13425 label: &'static str,
13426 checked: bool,
13427) {
13428 slider_checkbox_with_layout(
13429 ui,
13430 parent,
13431 name,
13432 label,
13433 checked,
13434 LayoutStyle::new().with_width_percent(1.0).with_height(30.0),
13435 );
13436}
13437
13438fn slider_checkbox_with_layout(
13439 ui: &mut UiDocument,
13440 parent: UiNodeId,
13441 name: &'static str,
13442 label: &'static str,
13443 checked: bool,
13444 layout: LayoutStyle,
13445) {
13446 let mut options = widgets::CheckboxOptions::default().with_action(name);
13447 options.layout = layout;
13448 options.text_style = text(12.0, color(220, 228, 238));
13449 widgets::checkbox(ui, parent, name, label, checked, options);
13450}
13451
13452fn choice_button(
13453 ui: &mut UiDocument,
13454 parent: UiNodeId,
13455 name: &'static str,
13456 label: &'static str,
13457 selected: bool,
13458) {
13459 let mut options =
13460 widgets::ButtonOptions::new(LayoutStyle::new().with_width(78.0).with_height(28.0))
13461 .with_action(name);
13462 options.visual = if selected {
13463 button_visual(48, 112, 184)
13464 } else {
13465 button_visual(38, 46, 58)
13466 };
13467 options.hovered_visual = Some(button_visual(65, 86, 106));
13468 options.pressed_visual = Some(button_visual(34, 54, 84));
13469 options.text_style = text(12.0, color(238, 244, 252));
13470 widgets::button(ui, parent, name, label, options);
13471}
13472
13473fn divider(ui: &mut UiDocument, parent: UiNodeId, name: &'static str) {
13474 ui.add_child(
13475 parent,
13476 UiNode::container(
13477 name,
13478 LayoutStyle::new()
13479 .with_width_percent(1.0)
13480 .with_height(1.0)
13481 .with_flex_shrink(0.0),
13482 )
13483 .with_visual(UiVisual::panel(color(48, 58, 72), None, 0.0)),
13484 );
13485}
13486
13487fn canvas(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
13488 let canvas_intrinsic = UiSize::new(720.0, 405.0);
13489 let body = section_with_min_viewport(ui, parent, "canvas", "Canvas", UiSize::new(720.0, 458.0));
13490 let controls = wrapping_row(ui, body, "canvas.options", 10.0);
13491 canvas_option_checkbox(
13492 ui,
13493 controls,
13494 "canvas.grow_horizontal",
13495 "Grow width",
13496 state.canvas_grow_horizontal,
13497 );
13498 canvas_option_checkbox(
13499 ui,
13500 controls,
13501 "canvas.grow_vertical",
13502 "Grow height",
13503 state.canvas_grow_vertical,
13504 );
13505 canvas_option_checkbox(
13506 ui,
13507 controls,
13508 "canvas.keep_aspect_ratio",
13509 "Keep aspect ratio",
13510 state.canvas_keep_aspect_ratio,
13511 );
13512
13513 let mut options = widgets::CanvasOptions::default()
13514 .with_accessibility_label("Shader canvas")
13515 .with_action("canvas.rotate")
13516 .with_intrinsic_size(canvas_intrinsic);
13517 options.action_mode = WidgetActionMode::Drag;
13518 if state.canvas_keep_aspect_ratio {
13519 options = options.with_aspect_ratio(16.0 / 9.0);
13520 }
13521 let canvas_width = if state.canvas_grow_horizontal {
13522 LayoutDimension::percent(1.0)
13523 } else {
13524 LayoutDimension::points(canvas_intrinsic.width)
13525 };
13526 let canvas_height = if state.canvas_grow_vertical {
13527 LayoutDimension::percent(1.0)
13528 } else {
13529 LayoutDimension::points(canvas_intrinsic.height)
13530 };
13531 options.layout = Layout::new()
13532 .size(LayoutSize::new(canvas_width, canvas_height))
13533 .min_size(LayoutSize::points(
13534 canvas_intrinsic.width,
13535 canvas_intrinsic.height,
13536 ))
13537 .flex(
13538 if state.canvas_grow_vertical { 1.0 } else { 0.0 },
13539 1.0,
13540 LayoutDimension::Auto,
13541 )
13542 .to_layout_style();
13543 options.visual = UiVisual::panel(
13544 color(18, 22, 28),
13545 Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
13546 4.0,
13547 );
13548 widgets::canvas(
13549 ui,
13550 body,
13551 "canvas.shader",
13552 CanvasContent::new("canvas.shader").program(showcase_canvas_program(state.cube)),
13553 options,
13554 );
13555}
13556
13557fn canvas_option_checkbox(
13558 ui: &mut UiDocument,
13559 parent: UiNodeId,
13560 name: &'static str,
13561 label: &'static str,
13562 checked: bool,
13563) {
13564 let mut options = widgets::CheckboxOptions::default()
13565 .with_action(name)
13566 .with_text_style(text(12.0, color(220, 228, 238)));
13567 options.layout = LayoutStyle::new().with_height(28.0).with_flex_shrink(0.0);
13568 widgets::checkbox(ui, parent, name, label, checked, options);
13569}
13570
13571fn showcase_canvas_program(cube: CanvasCubeState) -> CanvasRenderProgram {
13572 CanvasRenderProgram::wgsl(include_str!("shaders/showcase_canvas.wgsl"))
13573 .label("showcase.canvas")
13574 .constant("CUBE_YAW", cube.yaw as f64)
13575 .constant("CUBE_PITCH", cube.pitch as f64)
13576 .clear_color(Some(color(18, 22, 28)))
13577}
13578
13579fn section(
13580 ui: &mut UiDocument,
13581 parent: UiNodeId,
13582 name: impl Into<String>,
13583 _title: impl Into<String>,
13584) -> UiNodeId {
13585 section_with_min_viewport(ui, parent, name, _title, UiSize::ZERO)
13586}
13587
13588fn section_with_min_viewport(
13589 ui: &mut UiDocument,
13590 parent: UiNodeId,
13591 name: impl Into<String>,
13592 _title: impl Into<String>,
13593 min_viewport_size: UiSize,
13594) -> UiNodeId {
13595 let name = name.into();
13596 let layout = Layout::column()
13597 .size(LayoutSize::percent(1.0, 1.0))
13598 .min_size(LayoutSize::points(
13599 min_viewport_size.width.max(0.0),
13600 min_viewport_size.height.max(0.0),
13601 ))
13602 .gap(LayoutGap::points(10.0, 10.0))
13603 .flex(1.0, 1.0, LayoutDimension::Auto)
13604 .to_layout_style();
13605 widgets::scroll_area(
13606 ui,
13607 parent,
13608 format!("{name}.section_scroll"),
13609 ScrollAxes::BOTH,
13610 layout,
13611 )
13612}pub const fn width(self, width: LayoutDimension) -> Self
pub const fn height(self, height: LayoutDimension) -> Self
pub const fn to_taffy(self) -> TaffySize<Dimension>
pub fn from_taffy(size: TaffySize<Dimension>) -> Option<Self>
Trait Implementations§
Source§impl Clone for LayoutSize
impl Clone for LayoutSize
Source§fn clone(&self) -> LayoutSize
fn clone(&self) -> LayoutSize
Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
Performs copy-assignment from
source. Read moreSource§impl Debug for LayoutSize
impl Debug for LayoutSize
Source§impl From<LayoutSize> for Size<Dimension>
impl From<LayoutSize> for Size<Dimension>
Source§fn from(value: LayoutSize) -> Self
fn from(value: LayoutSize) -> Self
Converts to this type from the input type.
Source§impl PartialEq for LayoutSize
impl PartialEq for LayoutSize
Source§fn eq(&self, other: &LayoutSize) -> bool
fn eq(&self, other: &LayoutSize) -> bool
Tests for
self and other values to be equal, and is used by ==.impl Copy for LayoutSize
impl StructuralPartialEq for LayoutSize
Auto Trait Implementations§
impl Freeze for LayoutSize
impl RefUnwindSafe for LayoutSize
impl Send for LayoutSize
impl Sync for LayoutSize
impl Unpin for LayoutSize
impl UnsafeUnpin for LayoutSize
impl UnwindSafe for LayoutSize
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Convert
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can
then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Convert
Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be
further downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
Convert
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
Convert
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.