halley-wl 0.3.0

Wayland backend and rendering implementation for the Halley Wayland compositor.
use std::error::Error;

use smithay::{
    backend::renderer::{Color32F, gles::GlesFrame},
    utils::{Physical, Rectangle},
};

use crate::compositor::root::Halley;
use crate::render::draw_primitives::draw_rect;
use crate::text::{draw_ui_text_in, ui_text_size_in};

use super::{
    BANNER_EDGE_PAD, FOCUS_CYCLE_BACKDROP_ALPHA, FOCUS_CYCLE_CARD_PAD_X, FOCUS_CYCLE_GAP,
    FOCUS_CYCLE_ICON_PAD, FOCUS_CYCLE_LABEL_SCALE, FOCUS_CYCLE_META_SCALE,
    FOCUS_CYCLE_MONITOR_SCALE, FOCUS_CYCLE_VISIBLE_RADIUS, OverlayView, OverlayVisuals,
    draw_overflow_member_chip, draw_overlay_action_row, draw_overlay_chip,
    draw_overlay_chip_without_shadow, overlay_accent_fill, overlay_action_row_size,
    overlay_text_color_for_fill, resolve_overlay_visuals, truncate_overlay_text,
    truncate_overlay_text_to_width,
};

fn focus_cycle_card_size(distance: i32) -> (i32, i32) {
    match distance {
        0 => (244, 108),
        1 => (204, 92),
        _ => (168, 80),
    }
}

fn focus_cycle_label(overlay: &OverlayView<'_>, node_id: halley_core::field::NodeId) -> String {
    overlay
        .field
        .node(node_id)
        .map(|node| node.label.trim())
        .filter(|label| !label.is_empty())
        .map(str::to_string)
        .or_else(|| overlay.node_app_ids.get(&node_id).cloned())
        .unwrap_or_else(|| format!("window {}", node_id.as_u64()))
}

fn draw_focus_cycle_card(
    frame: &mut GlesFrame<'_, '_>,
    overlay: &OverlayView<'_>,
    visuals: &OverlayVisuals,
    rect: Rectangle<i32, Physical>,
    node_id: halley_core::field::NodeId,
    monitor: &str,
    selected: bool,
    distance: i32,
    damage: Rectangle<i32, Physical>,
) -> Result<(), Box<dyn Error>> {
    let fill = if selected {
        overlay_accent_fill(visuals, 0.46, 0.99)
    } else {
        visuals
            .palette
            .fill
            .mix(visuals.palette.border, 0.04 * (distance as f32 * 0.5))
            .alpha((0.78 - distance as f32 * 0.12).clamp(0.46, 0.78))
    };
    draw_overlay_chip(
        frame,
        overlay.render_state,
        visuals,
        rect,
        if selected { 20.0 } else { 18.0 },
        fill,
        true,
        damage,
        1.0,
    )?;

    let icon_size = (rect.size.h - FOCUS_CYCLE_ICON_PAD * 2).clamp(44, 72);
    let icon_rect = Rectangle::<i32, Physical>::new(
        (
            rect.loc.x + FOCUS_CYCLE_ICON_PAD,
            rect.loc.y + (rect.size.h - icon_size) / 2,
        )
            .into(),
        (icon_size, icon_size).into(),
    );
    let icon_fill = if selected {
        visuals.palette.key_fill.alpha(0.94)
    } else {
        visuals.palette.key_fill.alpha(0.84)
    };
    draw_overflow_member_chip(
        frame, overlay, visuals, node_id, icon_rect, icon_fill, 1.0, damage,
    )?;

    let raw_label = focus_cycle_label(overlay, node_id);
    let app_id = overlay
        .node_app_ids
        .get(&node_id)
        .map(String::as_str)
        .filter(|app_id| !app_id.trim().is_empty());
    let monitor_label = truncate_overlay_text(monitor, 10);
    let text_color = overlay_text_color_for_fill(fill, if selected { 1.0 } else { 0.96 });
    let subtext_color = visuals
        .palette
        .subtext
        .alpha(if selected { 0.98 } else { 0.90 });
    let text_x = icon_rect.loc.x + icon_rect.size.w + 12;
    let selection_label = "selected";
    let (selection_w, selection_h) = ui_text_size_in(
        overlay.render_state,
        &overlay.tuning.font,
        selection_label,
        FOCUS_CYCLE_META_SCALE,
    );
    let selection_rect = selected.then(|| {
        Rectangle::<i32, Physical>::new(
            (text_x, rect.loc.y + 10).into(),
            (selection_w + 16, selection_h + 8).into(),
        )
    });

    let badge_w = ui_text_size_in(
        overlay.render_state,
        &overlay.tuning.font,
        monitor_label.as_str(),
        FOCUS_CYCLE_MONITOR_SCALE,
    )
    .0 + 14;
    let text_max_w = (rect.loc.x + rect.size.w - badge_w - 22 - text_x).max(48);
    let label = truncate_overlay_text_to_width(
        overlay.render_state,
        &overlay.tuning.font,
        raw_label.as_str(),
        FOCUS_CYCLE_LABEL_SCALE,
        text_max_w,
    );
    let meta = app_id
        .filter(|app_id| !raw_label.eq_ignore_ascii_case(app_id))
        .map(|app_id| {
            truncate_overlay_text_to_width(
                overlay.render_state,
                &overlay.tuning.font,
                app_id,
                FOCUS_CYCLE_META_SCALE,
                text_max_w,
            )
        })
        .filter(|text| !text.is_empty());

    let (_, label_h) = ui_text_size_in(
        overlay.render_state,
        &overlay.tuning.font,
        label.as_str(),
        FOCUS_CYCLE_LABEL_SCALE,
    );
    let meta_h = meta
        .as_ref()
        .map(|text| {
            ui_text_size_in(
                overlay.render_state,
                &overlay.tuning.font,
                text.as_str(),
                FOCUS_CYCLE_META_SCALE,
            )
            .1
        })
        .unwrap_or(0);
    let (_, monitor_h) = ui_text_size_in(
        overlay.render_state,
        &overlay.tuning.font,
        monitor_label.as_str(),
        FOCUS_CYCLE_MONITOR_SCALE,
    );
    let selection_reserved_h = selection_rect
        .as_ref()
        .map(|rect| rect.size.h + 8)
        .unwrap_or(0);
    let total_h =
        selection_reserved_h + label_h + monitor_h + if meta.is_some() { meta_h + 8 } else { 4 };
    let base_y = (rect.loc.y + ((rect.size.h - total_h).max(0) / 2))
        .max(rect.loc.y + 10 + selection_reserved_h);

    let badge_rect = Rectangle::<i32, Physical>::new(
        (rect.loc.x + rect.size.w - badge_w - 12, rect.loc.y + 10).into(),
        (badge_w, monitor_h + 8).into(),
    );
    draw_overlay_chip_without_shadow(
        frame,
        overlay.render_state,
        visuals,
        badge_rect,
        10.0,
        visuals
            .palette
            .border
            .alpha(if selected { 0.95 } else { 0.74 }),
        false,
        damage,
        1.0,
    )?;
    draw_ui_text_in(
        frame,
        overlay.render_state,
        &overlay.tuning.font,
        badge_rect.loc.x + (badge_rect.size.w - (badge_w - 14)) / 2,
        badge_rect.loc.y + 4,
        monitor_label.as_str(),
        FOCUS_CYCLE_MONITOR_SCALE,
        overlay_text_color_for_fill(
            visuals
                .palette
                .border
                .alpha(if selected { 0.95 } else { 0.74 }),
            1.0,
        ),
        damage,
    )?;

    draw_ui_text_in(
        frame,
        overlay.render_state,
        &overlay.tuning.font,
        text_x.min(rect.loc.x + rect.size.w - FOCUS_CYCLE_CARD_PAD_X - 10),
        base_y,
        label.as_str(),
        FOCUS_CYCLE_LABEL_SCALE,
        text_color,
        damage,
    )?;
    if let Some(meta) = meta.as_ref() {
        draw_ui_text_in(
            frame,
            overlay.render_state,
            &overlay.tuning.font,
            text_x.min(rect.loc.x + rect.size.w - FOCUS_CYCLE_CARD_PAD_X - 10),
            base_y + label_h + 8,
            meta.as_str(),
            FOCUS_CYCLE_META_SCALE,
            subtext_color,
            damage,
        )?;
    }

    if let Some(select_rect) = selection_rect {
        let select_fill = visuals.palette.border.alpha(0.94);
        draw_overlay_chip_without_shadow(
            frame,
            overlay.render_state,
            visuals,
            select_rect,
            10.0,
            select_fill,
            false,
            damage,
            1.0,
        )?;
        draw_ui_text_in(
            frame,
            overlay.render_state,
            &overlay.tuning.font,
            select_rect.loc.x + 8,
            select_rect.loc.y + (select_rect.size.h - selection_h) / 2,
            selection_label,
            FOCUS_CYCLE_META_SCALE,
            overlay_text_color_for_fill(select_fill, 1.0),
            damage,
        )?;
    }

    Ok(())
}

pub(super) fn draw_focus_cycle_switcher(
    frame: &mut GlesFrame<'_, '_>,
    st: &mut Halley,
    screen_w: i32,
    screen_h: i32,
    damage: Rectangle<i32, Physical>,
) -> Result<bool, Box<dyn Error>> {
    let Some(session) = st.input.interaction_state.focus_cycle_session.as_ref() else {
        return Ok(false);
    };
    if session.candidates.len() < 2 {
        return Ok(false);
    }

    let visuals = resolve_overlay_visuals(&st.runtime.tuning);
    let overlay = OverlayView::from_halley(st);
    let len = session.candidates.len() as i32;
    let visible_count = len.min(FOCUS_CYCLE_VISIBLE_RADIUS * 2 + 1).max(2);
    let left_count = (visible_count - 1) / 2;
    let right_count = visible_count - 1 - left_count;
    let mut slots = Vec::new();
    for offset in -left_count..=right_count {
        let preview = session.preview_index as i32;
        let idx = (preview + offset).rem_euclid(len) as usize;
        slots.push((offset, session.candidates[idx]));
    }

    let widths = slots
        .iter()
        .map(|(offset, _)| focus_cycle_card_size(offset.abs()).0)
        .collect::<Vec<_>>();
    let total_w =
        widths.iter().sum::<i32>() + FOCUS_CYCLE_GAP * (slots.len().saturating_sub(1) as i32);
    let base_h = slots
        .iter()
        .map(|(offset, _)| focus_cycle_card_size(offset.abs()).1)
        .max()
        .unwrap_or(0);

    draw_rect(
        frame,
        0,
        0,
        screen_w.max(1),
        screen_h.max(1),
        Color32F::new(0.02, 0.03, 0.05, FOCUS_CYCLE_BACKDROP_ALPHA),
        damage,
    )?;

    let start_x = ((screen_w - total_w) / 2).max(BANNER_EDGE_PAD);
    let center_y = (screen_h as f32 * 0.52).round() as i32;
    let mut x = start_x;
    for (slot_index, (offset, node_id)) in slots.iter().enumerate() {
        let distance = offset.abs();
        let (w, h) = focus_cycle_card_size(distance);
        let y_offset = match distance {
            0 => 0,
            1 => 10,
            _ => 18,
        };
        let rect =
            Rectangle::<i32, Physical>::new((x, center_y - h / 2 + y_offset).into(), (w, h).into());
        let monitor = overlay
            .monitor_state
            .node_monitor
            .get(node_id)
            .map(String::as_str)
            .unwrap_or("?");
        draw_focus_cycle_card(
            frame,
            &overlay,
            &visuals,
            rect,
            *node_id,
            monitor,
            *offset == 0,
            distance,
            damage,
        )?;
        x += widths[slot_index] + FOCUS_CYCLE_GAP;
    }

    let actions = [
        ("Tab", "next"),
        ("Shift+Tab", "previous"),
        ("Esc", "cancel"),
    ];
    let (actions_w, _actions_h) =
        overlay_action_row_size(overlay.render_state, &overlay.tuning.font, &actions);
    let actions_x = ((screen_w - actions_w) / 2).max(BANNER_EDGE_PAD);
    let actions_y = center_y + base_h / 2 + 20;
    draw_overlay_action_row(
        frame,
        overlay.render_state,
        &visuals,
        &overlay.tuning.font,
        actions_x,
        actions_y,
        &actions,
        damage,
        0.96,
    )?;

    Ok(true)
}