cotis-layout 0.1.0-alpha

Flexbox-style layout engine for Cotis
Documentation
//! Demonstrates extending the `RenderTList` pipeline with a custom drawable type.
//!
//! `CircleDrawable` is appended to the end of the heterogeneous command list.
//! It implements [`FromRenderCommandOutput`] (via cotis-pipes) to draw a red circle centred on **every**
//! layout element, regardless of the element's background, text, or border content.
//! No changes to cotis, cotis-layout, or raylib-cotis internals are required.

use cotis::cotis_app::UiFn;
use cotis::element_configuring::{ConfigureElements, ConfiguredParentElement};
use cotis::utils::ElementIdConfig;
use cotis_defaults::colors::Color;
use cotis_defaults::element_configs::style::sizing::{AxisSizing, DoubleAxisSizing, Sizing};
use cotis_defaults::element_configs::style::types::{
    Alignment, LayoutAlignmentX, LayoutAlignmentY, LayoutDirection, Padding,
};
use cotis_defaults::element_configs::style::{
    BorderElementStyle, BorderWidthStyle, CornerRadiiStyle, CotisStyle, LayoutStyle,
};
use cotis_defaults::element_configs::text_config::{
    TextAlignment, TextConfig, TextElementConfig, TextElementConfigWrapMode, TextStyle,
};
use cotis_defaults::render_commands::render_t_list::RenderTList;
use cotis_defaults::render_commands::{Border, Image, Rectangle, Text};
use cotis_layout::layout_struct::RenderCommandOutput;
use cotis_pipes::cotis_layout_pipes::FromRenderCommandOutput;
use cotis_raylib::cotis_defaults_images::PathBasedImage;
use cotis_raylib::drawables::{RaylibDrawContext, RaylibDrawable};
use cotis_raylib::prelude::RaylibRender;
use raylib::consts::TextureFilter;
use raylib::prelude::RaylibDraw;

mod support;
use support::{
    ExampleConfig, compute_frame, empty_example_config, example_images, image_config, new_cotis_app,
};

fn main() {
    test_entry(RaylibRender::auto_start());
}

// ── Custom drawable ───────────────────────────────────────────────────────────

/// Draws a red circle centred on every layout element it receives,
/// ignoring what the element actually contains.
pub struct CircleDrawable {
    center_x: f32,
    center_y: f32,
    radius: f32,
}

impl RaylibDrawable for CircleDrawable {
    fn draw(self: Box<Self>, ctx: &mut RaylibDrawContext<'_, '_>) {
        ctx.d.draw_circle(
            self.center_x as i32,
            self.center_y as i32,
            self.radius,
            raylib::prelude::Color::new(220, 50, 50, 180),
        );
    }

    fn scale_by(&mut self, factor: f32) {
        self.center_x *= factor;
        self.center_y *= factor;
        self.radius *= factor;
    }
}

/// Fires for every `Element` variant regardless of its content.
impl<Custom> FromRenderCommandOutput<Custom> for CircleDrawable {
    fn decode_element(output: &mut RenderCommandOutput<Custom>) -> Option<Self> {
        if let RenderCommandOutput::Element(el) = output {
            let bb = el.bounds;
            Some(CircleDrawable {
                center_x: bb.x + bb.width / 2.0,
                center_y: bb.y + bb.height / 2.0,
                radius: (bb.width.min(bb.height) / 4.0).max(5.0),
            })
        } else {
            None
        }
    }
}

// ── Example entry point ───────────────────────────────────────────────────────

fn test_entry(mut renderer: RaylibRender) {
    let mut app = new_cotis_app(renderer, cotis_utils::math::Dimensions::new(1200.0, 800.0));
    let _font = app
        .render()
        .load_font_ex_and_keep(
            "./examples/fonts/Roboto-Regular.ttf",
            35,
            None,
            Some(TextureFilter::TEXTURE_FILTER_BILINEAR),
        )
        .unwrap();
    let ui = Demo {};

    type Cmds = RenderTList<
        Rectangle<'static, ()>,
        RenderTList<
            Image<'static, PathBasedImage, ()>,
            RenderTList<
                Border<'static, ()>,
                RenderTList<Text<'static, ()>, RenderTList<CircleDrawable, ()>>,
            >,
        >,
    >;

    while !app.render().window_should_close() {
        compute_frame::<Cmds, _>(&mut app, &ui);
    }
}

// ── UI definition ─────────────────────────────────────────────────────────────

struct Demo {}

impl<'inside, Conf> UiFn<'inside, Conf, ExampleConfig> for &Demo
where
    Conf: ConfigureElements<ExampleConfig>
        + cotis::element_configuring::ConfigureLeafElements<
            cotis_defaults::element_configs::text_config::TextElementConfig<'static>,
        >,
{
    fn draw_in(self, layout: &mut ConfiguredParentElement<'inside, Conf, ExampleConfig>) {
        draw_ui(layout);
    }
}

pub fn draw_ui<Conf>(parent: &mut ConfiguredParentElement<'_, Conf, ExampleConfig>)
where
    Conf: ConfigureElements<ExampleConfig>
        + cotis::element_configuring::ConfigureLeafElements<
            cotis_defaults::element_configs::text_config::TextElementConfig<'static>,
        >,
{
    let mut row = parent.new_element();
    row.set_config(ExampleConfig {
        id_config: ElementIdConfig::new_empty(),
        style: CotisStyle {
            sizing: Sizing::DoubleAxis(DoubleAxisSizing {
                width: AxisSizing::Grow(0.0, f32::MAX),
                height: AxisSizing::Fit(0.0, f32::MAX),
            }),
            layout: LayoutStyle {
                padding: Padding {
                    top: 20.0,
                    bottom: 20.0,
                    left: 20.0,
                    right: 20.0,
                },
                child_gap: 16.0,
                child_alignment: Alignment {
                    x: LayoutAlignmentX::Left,
                    y: LayoutAlignmentY::Center,
                },
                layout_direction: LayoutDirection::LeftToRight,
            },
            background_color: Color::rgba(30.0, 30.0, 40.0, 255.0),
            border: BorderElementStyle {
                color: Color::rgba(80.0, 80.0, 120.0, 255.0),
                width: BorderWidthStyle {
                    left: 2.0,
                    right: 2.0,
                    top: 2.0,
                    bottom: 2.0,
                    between_children: 0.0,
                },
            },
            corner_radius: CornerRadiiStyle {
                top_left: 8.0,
                top_right: 8.0,
                bottom_left: 8.0,
                bottom_right: 8.0,
            },
            floating: None,
            ..Default::default()
        },
        image: None,
        custom: None,
        extra_data: None,
    });
    let mut row_p = row.make_parent();

    add_card(
        &mut row_p,
        "Hello",
        Color::rgba(60.0, 20.0, 20.0, 255.0),
        example_images::apple_png(),
    );
    add_card(
        &mut row_p,
        "World",
        Color::rgba(20.0, 60.0, 20.0, 255.0),
        example_images::apple_jpeg(),
    );
    add_card(
        &mut row_p,
        "Circle!",
        Color::rgba(20.0, 20.0, 60.0, 255.0),
        example_images::calendar_svg(),
    );

    row_p.close_element();
}

fn add_card<Conf>(
    parent: &mut ConfiguredParentElement<'_, Conf, ExampleConfig>,
    label: &str,
    bg: Color,
    image: PathBasedImage,
) where
    Conf: ConfigureElements<ExampleConfig>
        + cotis::element_configuring::ConfigureLeafElements<
            cotis_defaults::element_configs::text_config::TextElementConfig<'static>,
        >,
{
    let mut card = parent.new_element();
    card.set_config(ExampleConfig {
        id_config: ElementIdConfig::new_empty(),
        style: CotisStyle {
            sizing: Sizing::DoubleAxis(DoubleAxisSizing {
                width: AxisSizing::Fixed(160.0),
                height: AxisSizing::Fixed(160.0),
            }),
            layout: LayoutStyle {
                padding: Padding {
                    top: 8.0,
                    bottom: 8.0,
                    left: 8.0,
                    right: 8.0,
                },
                child_gap: 0.0,
                child_alignment: Alignment {
                    x: LayoutAlignmentX::Center,
                    y: LayoutAlignmentY::Center,
                },
                layout_direction: LayoutDirection::LeftToRight,
            },
            background_color: bg,
            corner_radius: CornerRadiiStyle {
                top_left: 6.0,
                top_right: 6.0,
                bottom_left: 6.0,
                bottom_right: 6.0,
            },
            ..Default::default()
        },
        image: Some(image_config(image)),
        custom: None,
        extra_data: None,
    });
    let mut card_p = card.make_parent();

    let mut text_el = card_p.new_element();
    text_el.set_config(empty_example_config());
    text_el.set_leaf_config(TextElementConfig {
        text: cotis::utils::OwnedOrRef::Owned(label.to_owned().into_boxed_str()),
        config: TextConfig {
            font_id: 0,
            font_size: 18.0,
            letter_spacing: 1.0,
            line_height: 0.0,
            wrap_mode: TextElementConfigWrapMode::Words,
            alignment: TextAlignment::Left,
        },
        style: TextStyle {
            color: Color::rgba(240.0, 240.0, 240.0, 255.0),
        },
    });

    card_p.close_element();
}