textual 1.0.0-dev

A reactive TUI framework inspired by the Python Textual library
Documentation
use rich_rs::{MetaValue, Segments};

use crate::node_id::{NodeId, node_id_to_ffi};
use crate::renderables::{TextOpacity, Tint};
use crate::style::Style;

pub(crate) fn apply_style_to_segments(
    widget_id: NodeId,
    segments: Segments,
    style: Style,
    parent_style: Option<Style>,
) -> Segments {
    if style.is_empty() {
        return segments;
    }
    let rich_attrs = style.to_rich_without_colors();
    let fallback_bg = crate::style::parse_color_like("$background");
    let parent_bg = crate::css::current_composited_background()
        .or_else(|| parent_style.and_then(|s| s.bg))
        .or(fallback_bg);
    segments
        .into_iter()
        .map(|mut seg| {
            if seg.control.is_some() {
                return seg;
            }

            // Only apply this widget's resolved style to segments that originate from this widget.
            // Child widgets render their own styles already (including inherited properties), and
            // parent widgets should not overwrite them during this pass.
            if let Some(meta) = seg.meta.as_ref().and_then(|meta| meta.meta.as_ref()) {
                if let Some(MetaValue::Int(value)) = meta.get("textual:widget_id") {
                    if *value != node_id_to_ffi(widget_id) as i64 {
                        return seg;
                    }
                }
            }

            let mut no_text_style = false;
            if let Some(meta) = seg.meta.as_ref().and_then(|meta| meta.meta.as_ref()) {
                if let Some(MetaValue::Bool(true)) = meta.get("textual:no_style") {
                    return seg;
                }
                if let Some(MetaValue::Bool(true)) = meta.get("textual:no_text_style") {
                    no_text_style = true;
                }
                if let Some(MetaValue::Bool(true)) = meta.get("textual:no_bg") {
                    // We'll clear bgcolor after composing below.
                }
            }
            if !no_text_style {
                if let Some(rich_attrs) = rich_attrs {
                    seg.style = Some(match seg.style {
                        Some(existing) => rich_attrs.combine(&existing),
                        None => rich_attrs,
                    });
                }
            }
            let mut no_bg = false;
            if let Some(meta) = seg.meta.as_ref().and_then(|meta| meta.meta.as_ref()) {
                if let Some(MetaValue::Bool(true)) = meta.get("textual:no_bg") {
                    no_bg = true;
                }
            }

            let mut style_changed = false;
            let mut s = seg.style.unwrap_or_else(rich_rs::Style::new);
            // In composition terms, terminal-default background should behave as transparent
            // so children can inherit parent/widget surfaces.
            let explicit_bg = s.bgcolor.and_then(|bg| {
                if matches!(bg, rich_rs::SimpleColor::Default) {
                    None
                } else {
                    Some(bg)
                }
            });
            let mut under_bg = explicit_bg
                .map(crate::style::color_from_simple)
                .or(parent_bg)
                .unwrap_or(crate::style::Color::rgb(0, 0, 0));

            if !no_bg {
                if explicit_bg.is_none() {
                    // Preserve per-segment backgrounds (e.g. DataTable row/cell backgrounds,
                    // Input selection/cursor). When a segment has no explicit background:
                    // - apply this widget's own `bg` if present, flattened over parent bg
                    // - otherwise keep the parent surface color so transparent children
                    //   visually inherit container background during composition.
                    let effective_bg = if let Some(bg) = style.bg {
                        bg.flatten_over(under_bg)
                    } else {
                        under_bg
                    };
                    under_bg = effective_bg;
                    s.bgcolor = Some(effective_bg.to_simple_opaque());
                    style_changed = true;
                }
            } else if s.bgcolor.is_some() {
                s.bgcolor = None;
                style_changed = true;
            }

            if let Some(tint) = style.background_tint {
                if let Some(bg) = s.bgcolor {
                    let bg = crate::style::color_from_simple(bg);
                    let blended =
                        Tint::<()>::blend_color_with_percent(bg, tint.color, tint.percent);
                    let flat = blended.flatten_over(under_bg);
                    under_bg = flat;
                    s.bgcolor = Some(flat.to_simple_opaque());
                    style_changed = true;
                }
            }
            let text_opacity = style.text_opacity.map(|value| value as f32 / 100.0);
            // Preserve per-segment foregrounds unless unset.
            if s.color.is_none() {
                let bg_for_text = s
                    .bgcolor
                    .map(crate::style::color_from_simple)
                    .unwrap_or(under_bg);

                if let Some(fg) = style.fg {
                    let mut fg = fg;
                    if let Some(opacity) = text_opacity {
                        fg = TextOpacity::<()>::apply_alpha(fg, opacity);
                    }
                    let flat = fg.flatten_over(bg_for_text);
                    s.color = Some(flat.to_simple_opaque());
                    style_changed = true;
                } else if let Some(auto) = style.fg_auto {
                    let auto_alpha = auto.alpha();
                    let effective_alpha = if let Some(opacity) = text_opacity {
                        (auto_alpha * opacity).clamp(0.0, 1.0)
                    } else {
                        auto_alpha
                    };
                    let contrast =
                        crate::style::contrast_text(bg_for_text).with_alpha(effective_alpha);
                    let flat = contrast.flatten_over(bg_for_text);
                    s.color = Some(flat.to_simple_opaque());
                    style_changed = true;
                }
            } else if let (Some(opacity), Some(existing)) = (text_opacity, s.color) {
                let bg_for_text = s
                    .bgcolor
                    .map(crate::style::color_from_simple)
                    .unwrap_or(under_bg);
                let existing = crate::style::color_from_simple(existing);
                let flat = TextOpacity::<()>::blend_foreground_over_background(
                    existing,
                    bg_for_text,
                    opacity,
                );
                s.color = Some(flat.to_simple_opaque());
                style_changed = true;
            }
            if let Some(tint) = style.tint {
                if let Some(bg) = s.bgcolor {
                    let bg = crate::style::color_from_simple(bg);
                    let blended =
                        Tint::<()>::blend_color_with_percent(bg, tint.color, tint.percent);
                    s.bgcolor = Some(blended.to_simple_opaque());
                    style_changed = true;
                }
                if let Some(fg) = s.color {
                    let fg = crate::style::color_from_simple(fg);
                    let blended =
                        Tint::<()>::blend_color_with_percent(fg, tint.color, tint.percent);
                    s.color = Some(blended.to_simple_opaque());
                    style_changed = true;
                }
            }
            if style_changed || seg.style.is_some() {
                seg.style = Some(s);
            }
            seg
        })
        .collect()
}

pub(crate) fn apply_widget_opacity_to_segments(
    segments: Segments,
    opacity_percent: u8,
    parent_style: Option<Style>,
) -> Segments {
    if opacity_percent >= 100 {
        return segments;
    }
    let opacity = (opacity_percent as f32 / 100.0).clamp(0.0, 1.0);
    let fallback_bg = crate::style::parse_color_like("$background");
    let parent_bg = crate::css::current_composited_background()
        .or_else(|| parent_style.and_then(|style| style.bg))
        .or(fallback_bg)
        .unwrap_or(crate::style::Color::rgb(0, 0, 0));

    segments
        .into_iter()
        .map(|mut seg| {
            if seg.control.is_some() {
                return seg;
            }
            let mut style_changed = false;
            let mut style = seg.style.unwrap_or_else(rich_rs::Style::new);
            let original_bg = style.bgcolor.map(crate::style::color_from_simple);
            let original_fg = style.color.map(crate::style::color_from_simple);

            if let Some(bg) = original_bg {
                let bg = TextOpacity::<()>::apply_alpha(bg, opacity);
                let flat_bg = bg.flatten_over(parent_bg);
                style.bgcolor = Some(flat_bg.to_simple_opaque());
                style_changed = true;
            }

            if let Some(fg) = original_fg {
                let fg_source = fg;
                let fg = TextOpacity::<()>::apply_alpha(fg, opacity);
                let mut flat_fg = fg.flatten_over(parent_bg);
                if let (Some(src_bg), Some(dst_bg)) = (
                    original_bg,
                    style.bgcolor.map(crate::style::color_from_simple),
                ) {
                    if src_bg == fg_source {
                        flat_fg = dst_bg;
                    }
                }
                style.color = Some(flat_fg.to_simple_opaque());
                style_changed = true;
            }

            if style_changed || seg.style.is_some() {
                seg.style = Some(style);
            }
            seg
        })
        .collect()
}