use std::error::Error;
use std::f32::consts::{PI, TAU};
use smithay::{
backend::renderer::{
Texture,
gles::{GlesFrame, Uniform},
},
utils::{Buffer, Physical, Rectangle, Transform},
};
use crate::compositor::root::Halley;
use crate::overlay::{ClusterBloomAnimSnapshot, OverlayView};
use crate::presentation::{themed_node_fill_color, themed_node_label_text_color};
use crate::render::app_icon::ensure_app_icon_resources_for_node_ids;
use crate::render::state::NodeAppIconCacheEntry;
use crate::render::{node_app_icon_fallback_glyph, node_app_icon_texture_allowed};
use crate::text::{draw_ui_text_in, ui_text_size_in};
#[derive(Clone, Copy)]
pub(crate) struct BloomTokenLayout {
pub(crate) cluster_id: halley_core::cluster::ClusterId,
pub(crate) member_id: halley_core::field::NodeId,
pub(crate) center_sx: i32,
pub(crate) center_sy: i32,
pub(crate) token_radius: i32,
pub(crate) core_sx: i32,
pub(crate) core_sy: i32,
}
pub(crate) fn ensure_cluster_bloom_icon_resources(
renderer: &mut smithay::backend::renderer::gles::GlesRenderer,
st: &mut Halley,
monitor: &str,
) -> Result<(), Box<dyn Error>> {
if !st.runtime.tuning.cluster_show_icons
|| !node_app_icon_texture_allowed(st.runtime.tuning.node_show_app_icons, false)
{
return Ok(());
}
let overlay = OverlayView::from_halley(st);
let ids = cluster_bloom_layouts(&overlay, 1, 1, monitor)
.into_iter()
.map(|layout| layout.member_id);
ensure_app_icon_resources_for_node_ids(renderer, st, ids)
}
pub(crate) fn cluster_bloom_layouts(
overlay: &OverlayView<'_>,
screen_w: i32,
screen_h: i32,
monitor: &str,
) -> Vec<BloomTokenLayout> {
let Some(cid) = overlay
.cluster_state
.cluster_bloom_open
.get(monitor)
.copied()
else {
return Vec::new();
};
cluster_bloom_layouts_for_cluster(
overlay,
screen_w,
screen_h,
ClusterBloomAnimSnapshot {
cluster_id: cid,
mix: 1.0,
},
monitor,
)
}
fn cluster_bloom_layouts_for_cluster(
overlay: &OverlayView<'_>,
screen_w: i32,
screen_h: i32,
snapshot: ClusterBloomAnimSnapshot,
monitor: &str,
) -> Vec<BloomTokenLayout> {
if overlay
.cluster_state
.active_cluster_workspaces
.get(monitor)
.is_some()
{
return Vec::new();
}
let Some(cluster) = overlay.field.cluster(snapshot.cluster_id) else {
return Vec::new();
};
if !cluster.is_collapsed() {
return Vec::new();
}
let Some(core_id) = cluster.core else {
return Vec::new();
};
let Some(core) = overlay.field.node(core_id) else {
return Vec::new();
};
let (core_sx, core_sy) =
overlay.world_to_screen(screen_w.max(1), screen_h.max(1), core.pos.x, core.pos.y);
let mut members = cluster.members().to_vec();
members.sort_by_key(|id| id.as_u64());
let count = members.len().max(1);
let token_radius = 24;
let token_diameter = token_radius as f32 * 2.0;
let min_slots = 10usize;
let slots = count.max(min_slots);
let angle_step = TAU / slots as f32;
let min_chord = token_diameter + 18.0;
let bloom_radius = (min_chord / (2.0 * (angle_step * 0.5).sin()).max(0.20)).max(84.0)
+ (count as f32 - 1.0).min(5.0) * 3.0;
let direction = match overlay.tuning.cluster_bloom_direction {
halley_config::ClusterBloomDirection::Clockwise => 1.0,
halley_config::ClusterBloomDirection::CounterClockwise => -1.0,
};
let mix = snapshot.mix.clamp(0.0, 1.0);
let eased = mix * mix * (3.0 - 2.0 * mix);
members
.into_iter()
.enumerate()
.map(|(index, member_id)| {
let angle = -PI * 0.5 + direction * (angle_step * index as f32);
let radial_dx = angle.cos() * bloom_radius * eased;
let radial_dy = angle.sin() * bloom_radius * eased;
let center_sx = (core_sx as f32 + radial_dx).round() as i32;
let center_sy = (core_sy as f32 + radial_dy).round() as i32;
BloomTokenLayout {
cluster_id: snapshot.cluster_id,
member_id,
center_sx,
center_sy,
token_radius,
core_sx,
core_sy,
}
})
.collect()
}
pub(crate) fn bloom_token_hit_test(
st: &Halley,
screen_w: i32,
screen_h: i32,
monitor: &str,
sx: f32,
sy: f32,
) -> Option<BloomTokenLayout> {
let overlay = OverlayView::from_halley(st);
cluster_bloom_layouts(&overlay, screen_w, screen_h, monitor)
.into_iter()
.find(|layout| {
let dx = sx - layout.center_sx as f32;
let dy = sy - layout.center_sy as f32;
dx * dx + dy * dy <= (layout.token_radius * layout.token_radius) as f32
})
}
pub(crate) fn draw_cluster_bloom(
frame: &mut GlesFrame<'_, '_>,
st: &mut Halley,
screen_w: i32,
screen_h: i32,
monitor: &str,
damage: Rectangle<i32, Physical>,
) -> Result<(), Box<dyn Error>> {
let Some(snapshot) = st.ui.render_state.cluster_bloom_snapshot_for_monitor(
monitor,
st.model
.cluster_state
.cluster_bloom_open
.get(monitor)
.copied(),
) else {
let overlay = OverlayView::from_halley(st);
draw_cluster_join_affordance(frame, &overlay, screen_w, screen_h, monitor, damage)?;
return Ok(());
};
let overlay = OverlayView::from_halley(st);
let bloom_alpha = snapshot.mix.clamp(0.0, 1.0);
for layout in cluster_bloom_layouts_for_cluster(&overlay, screen_w, screen_h, snapshot, monitor)
{
draw_bloom_token(frame, &overlay, &layout, bloom_alpha, damage)?;
}
draw_cluster_join_affordance(frame, &overlay, screen_w, screen_h, monitor, damage)?;
Ok(())
}
fn draw_bloom_token(
frame: &mut GlesFrame<'_, '_>,
overlay: &OverlayView<'_>,
layout: &BloomTokenLayout,
alpha: f32,
damage: Rectangle<i32, Physical>,
) -> Result<(), Box<dyn Error>> {
if alpha <= 0.01 {
return Ok(());
}
let preview = overlay
.interaction_state
.bloom_pull_preview
.as_ref()
.filter(|preview| {
preview.monitor == overlay.monitor_state.current_monitor
&& preview.cluster_id == layout.cluster_id
&& preview.member_id == layout.member_id
})
.cloned();
let display_offset = preview
.as_ref()
.map(|preview| preview.display_offset)
.unwrap_or(halley_core::field::Vec2 { x: 0.0, y: 0.0 });
let tether_origin = preview
.as_ref()
.map(|preview| preview.slot_screen)
.unwrap_or(halley_core::field::Vec2 {
x: layout.center_sx as f32,
y: layout.center_sy as f32,
});
let pull_mix = crate::compositor::interaction::state::bloom_pull_display_mix(display_offset);
let hold_progress = preview
.as_ref()
.map(|preview| preview.hold_progress)
.unwrap_or(0.0);
let pre_release_mix = bloom_pre_release_mix(hold_progress);
let radius = (layout.token_radius as f32
+ 5.0 * pull_mix
+ 1.5 * hold_progress
+ 12.0 * pre_release_mix)
.round()
.max(1.0) as i32;
let center_sx = layout.center_sx + display_offset.x.round() as i32;
let center_sy = layout.center_sy + display_offset.y.round() as i32;
if pull_mix > 0.01 {
draw_bloom_anchor_dot(
frame,
overlay,
tether_origin.x.round() as i32,
tether_origin.y.round() as i32,
alpha,
pull_mix,
pre_release_mix,
damage,
)?;
}
let Some(texture) = overlay.render_state.gpu.node_circle_texture.as_ref() else {
return Ok(());
};
let Some(program) = overlay.render_state.gpu.node_circle_program.as_ref() else {
return Ok(());
};
let diameter = radius * 2;
let dest = Rectangle::<i32, Physical>::new(
(center_sx - radius, center_sy - radius).into(),
(diameter, diameter).into(),
);
let tex_size: smithay::utils::Size<i32, Buffer> = texture.size();
let src = Rectangle::<f64, Buffer>::new(
(0.0, 0.0).into(),
(tex_size.w as f64, tex_size.h as f64).into(),
);
let fill_color = themed_node_fill_color(overlay.tuning, false);
let uniforms = [
Uniform::new(
"node_color",
(fill_color.r(), fill_color.g(), fill_color.b(), 0.0f32),
),
Uniform::new(
"fill_color",
(fill_color.r(), fill_color.g(), fill_color.b(), 1.0f32),
),
Uniform::new("flat_fill", 1.0f32),
Uniform::new("center_flat_fill", 1.0f32),
];
frame.render_texture_from_to(
texture,
src,
dest,
&[damage],
&[],
Transform::Normal,
alpha,
Some(program),
&uniforms,
)?;
if overlay.tuning.cluster_show_icons
&& node_app_icon_texture_allowed(overlay.tuning.node_show_app_icons, false)
&& let Some(NodeAppIconCacheEntry::Ready(icon)) =
overlay.node_app_icon_entry(layout.member_id)
{
let side = (diameter as f32 * 0.64).round() as i32;
let icon_dest = Rectangle::<i32, Physical>::new(
(center_sx - side / 2, center_sy - side / 2).into(),
(side, side).into(),
);
let icon_src = Rectangle::<f64, Buffer>::new(
(0.0, 0.0).into(),
(icon.width as f64, icon.height as f64).into(),
);
frame.render_texture_from_to(
&icon.texture,
icon_src,
icon_dest,
&[damage],
&[],
Transform::Normal,
alpha,
None,
&[],
)?;
return Ok(());
}
let glyph = node_app_icon_fallback_glyph(
overlay
.node_app_ids
.get(&layout.member_id)
.map(String::as_str),
overlay
.field
.node(layout.member_id)
.map(|n| n.label.as_str())
.unwrap_or("?"),
)
.to_string();
let scale = 2;
let (text_w, text_h) =
ui_text_size_in(overlay.render_state, &overlay.tuning.font, &glyph, scale);
draw_ui_text_in(
frame,
overlay.render_state,
&overlay.tuning.font,
center_sx - text_w / 2,
center_sy - text_h / 2,
&glyph,
scale,
themed_node_label_text_color(fill_color, alpha),
damage,
)?;
Ok(())
}
fn bloom_pre_release_mix(hold_progress: f32) -> f32 {
let t = ((hold_progress - 0.62) / 0.38).clamp(0.0, 1.0);
t * t * (3.0 - 2.0 * t)
}
fn draw_bloom_anchor_dot(
frame: &mut GlesFrame<'_, '_>,
overlay: &OverlayView<'_>,
x: i32,
y: i32,
alpha: f32,
pull_mix: f32,
pre_release_mix: f32,
damage: Rectangle<i32, Physical>,
) -> Result<(), Box<dyn Error>> {
let Some(texture) = overlay.render_state.gpu.node_circle_texture.as_ref() else {
return Ok(());
};
let Some(program) = overlay.render_state.gpu.node_circle_program.as_ref() else {
return Ok(());
};
let radius = (5.0 + 2.0 * pull_mix + 2.5 * pre_release_mix).round() as i32;
let rect = Rectangle::<i32, Physical>::new(
(x - radius, y - radius).into(),
((radius * 2).max(1), (radius * 2).max(1)).into(),
);
let tex_size: smithay::utils::Size<i32, Buffer> = texture.size();
let src = Rectangle::<f64, Buffer>::new(
(0.0, 0.0).into(),
(tex_size.w as f64, tex_size.h as f64).into(),
);
let dot_alpha = alpha * (0.84 + 0.10 * pull_mix + 0.12 * pre_release_mix).clamp(0.0, 1.0);
let fill_color = themed_node_fill_color(overlay.tuning, false);
let uniforms = [
Uniform::new(
"node_color",
(fill_color.r(), fill_color.g(), fill_color.b(), 0.0f32),
),
Uniform::new(
"fill_color",
(fill_color.r(), fill_color.g(), fill_color.b(), 1.0f32),
),
Uniform::new("flat_fill", 1.0f32),
Uniform::new("center_flat_fill", 1.0f32),
];
frame.render_texture_from_to(
texture,
src,
rect,
&[damage],
&[],
Transform::Normal,
dot_alpha,
Some(program),
&uniforms,
)?;
Ok(())
}
fn draw_cluster_join_affordance(
frame: &mut GlesFrame<'_, '_>,
overlay: &OverlayView<'_>,
screen_w: i32,
screen_h: i32,
monitor: &str,
damage: Rectangle<i32, Physical>,
) -> Result<(), Box<dyn Error>> {
let Some(candidate) = overlay.interaction_state.cluster_join_candidate.as_ref() else {
return Ok(());
};
if candidate.monitor != monitor {
return Ok(());
}
if !candidate.ready {
return Ok(());
}
let Some(cluster) = overlay.field.cluster(candidate.cluster_id) else {
return Ok(());
};
let Some(core_id) = cluster.core else {
return Ok(());
};
let Some(core) = overlay.field.node(core_id) else {
return Ok(());
};
let (sx, sy) = overlay.world_to_screen(screen_w, screen_h, core.pos.x, core.pos.y);
let radius = 30;
let rect = Rectangle::<i32, Physical>::new(
(sx - radius, sy - radius).into(),
(radius * 2, radius * 2).into(),
);
let Some(texture) = overlay.render_state.gpu.node_circle_texture.as_ref() else {
return Ok(());
};
let Some(program) = overlay.render_state.gpu.node_circle_program.as_ref() else {
return Ok(());
};
let tex_size: smithay::utils::Size<i32, Buffer> = texture.size();
let src = Rectangle::<f64, Buffer>::new(
(0.0, 0.0).into(),
(tex_size.w as f64, tex_size.h as f64).into(),
);
let fill_color = themed_node_fill_color(overlay.tuning, true);
let uniforms = [
Uniform::new(
"node_color",
(fill_color.r(), fill_color.g(), fill_color.b(), 0.0f32),
),
Uniform::new(
"fill_color",
(fill_color.r(), fill_color.g(), fill_color.b(), 1.0f32),
),
Uniform::new("flat_fill", 1.0f32),
Uniform::new("center_flat_fill", 1.0f32),
];
frame.render_texture_from_to(
texture,
src,
rect,
&[damage],
&[],
Transform::Normal,
0.9,
Some(program),
&uniforms,
)?;
Ok(())
}