Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Pipeline

Group: Data

Code

pub struct PipelineDemo {
    stages: Vec<Vec<PipelineStage>>,
    focused_stage: Vec<Option<usize>>,
    last_message: String,
}

fn in_progress_stages() -> Vec<PipelineStage> {
    vec![
        PipelineStage {
            label: "Checkout".into(),
            status: StageStatus::Done,
            action: None,
        },
        PipelineStage {
            label: "Build".into(),
            status: StageStatus::Done,
            action: None,
        },
        PipelineStage {
            label: "Test".into(),
            status: StageStatus::Active,
            action: Some("Retry".into()),
        },
        PipelineStage {
            label: "Deploy".into(),
            status: StageStatus::Pending,
            action: Some("Go".into()),
        },
        PipelineStage {
            label: "Notify".into(),
            status: StageStatus::Pending,
            action: Some("Skip".into()),
        },
    ]
}

fn failed_stages() -> Vec<PipelineStage> {
    vec![
        PipelineStage {
            label: "Checkout".into(),
            status: StageStatus::Done,
            action: None,
        },
        PipelineStage {
            label: "Build".into(),
            status: StageStatus::Failed,
            action: Some("Retry".into()),
        },
        PipelineStage {
            label: "Test".into(),
            status: StageStatus::Failed,
            action: Some("Retry".into()),
        },
        PipelineStage {
            label: "Deploy".into(),
            status: StageStatus::Pending,
            action: Some("Go".into()),
        },
        PipelineStage {
            label: "Notify".into(),
            status: StageStatus::Pending,
            action: Some("Skip".into()),
        },
    ]
}

impl PipelineDemo {
    pub fn new() -> Self {
        Self {
            stages: vec![in_progress_stages(), failed_stages()],
            // Index 2 is the stage with something to act on in both
            // variants ("Test" is Active in the in-progress variant,
            // Failed in the failed one) — focusing it by default means
            // `Enter` has an immediate, visible effect without the user
            // first pressing `→` twice.
            focused_stage: vec![Some(2), Some(2)],
            last_message: "←/→=focus  Enter=action  r=reset".into(),
        }
    }

    fn view(&self, variant: usize) -> PipelineView {
        PipelineView {
            id: WidgetId::new("gallery:pipeline"),
            stages: self.stages[variant].clone(),
            focused_stage: self.focused_stage[variant],
        }
    }

    fn pipeline_rect(area: Rect, backend: &dyn Backend) -> Rect {
        let lh = backend.line_height();
        let pv_h = (lh * 5.0).min((area.height - lh).max(0.0));
        let pv_y = area.y + ((area.height - lh - pv_h) / 2.0).max(0.0);
        let margin = (lh * 2.0).min(area.width / 2.0);
        Rect::new(
            area.x + margin,
            pv_y,
            (area.width - margin * 2.0).max(0.0),
            pv_h,
        )
    }

    fn status_rect(area: Rect, backend: &dyn Backend) -> Rect {
        let lh = backend.line_height();
        Rect::new(area.x, area.y + area.height - lh, area.width, lh)
    }

    fn status(&self) -> StatusBar {
        StatusBar {
            id: WidgetId::new("gallery:pipeline:status"),
            left_segments: vec![StatusBarSegment {
                text: format!("  {} ", self.last_message),
                fg: Color::rgb(255, 255, 255),
                bg: Color::rgb(40, 80, 120),
                bold: false,
                action_id: None,
            }],
            right_segments: vec![],
        }
    }

    fn apply_event(&mut self, variant: usize, event: PipelineEvent) {
        match event {
            PipelineEvent::StageAction { index } => {
                let label = self.stages[variant][index].label.clone();
                let action = self.stages[variant][index]
                    .action
                    .clone()
                    .unwrap_or_default();
                self.last_message = format!("{action} on '{label}'");
                if self.stages[variant][index].status == StageStatus::Active
                    || self.stages[variant][index].status == StageStatus::Failed
                {
                    self.stages[variant][index].status = StageStatus::Done;
                }
            }
            PipelineEvent::StageSelected { index } => {
                self.last_message = format!("Stage {index}: {}", self.stages[variant][index].label);
            }
            PipelineEvent::KeyPressed { key, .. } => {
                self.last_message = format!("Key: {key}");
            }
        }
    }
}

impl Default for PipelineDemo {
    fn default() -> Self {
        Self::new()
    }
}

impl Demo for PipelineDemo {
    fn name(&self) -> &'static str {
        "Pipeline"
    }

    fn group(&self) -> &'static str {
        "Data"
    }

    fn variants(&self) -> &'static [&'static str] {
        &["In progress", "Failed"]
    }

    fn render(&self, variant: usize, backend: &mut dyn Backend, area: Rect) {
        let pv_rect = Self::pipeline_rect(area, backend);
        let _ = backend.draw_pipeline_view(pv_rect, &self.view(variant));

        let status_rect = Self::status_rect(area, backend);
        let _ = backend.draw_status_bar_interactive(
            status_rect,
            &self.status(),
            &InteractionState::new(),
        );
    }

    fn handle(
        &mut self,
        variant: usize,
        event: &UiEvent,
        backend: &mut dyn Backend,
        area: Rect,
    ) -> Reaction {
        match event {
            UiEvent::KeyPressed {
                key: Key::Char('r'),
                ..
            } => {
                self.stages[variant] = if variant == 0 {
                    in_progress_stages()
                } else {
                    failed_stages()
                };
                self.focused_stage[variant] = Some(2);
                self.last_message = "Reset".into();
                Reaction::Redraw
            }
            UiEvent::KeyPressed { key, modifiers, .. } => {
                let key_str = match key {
                    Key::Named(quadraui::NamedKey::Left) => "Left",
                    Key::Named(quadraui::NamedKey::Right) => "Right",
                    Key::Named(quadraui::NamedKey::Enter) => "Enter",
                    Key::Char(c) => {
                        let s = c.to_string();
                        let mut view = self.view(variant);
                        return match view.handle_key(&s, *modifiers) {
                            Some(ev) => {
                                self.apply_event(variant, ev);
                                self.focused_stage[variant] = view.focused_stage;
                                Reaction::Redraw
                            }
                            None => Reaction::Continue,
                        };
                    }
                    _ => return Reaction::Continue,
                };
                let mut view = self.view(variant);
                if let Some(ev) = view.handle_key(key_str, *modifiers) {
                    self.apply_event(variant, ev);
                }
                self.focused_stage[variant] = view.focused_stage;
                Reaction::Redraw
            }
            UiEvent::MouseDown { position, .. } => {
                let pv_rect = Self::pipeline_rect(area, backend);
                let view = self.view(variant);
                let layout = backend.pipeline_view_layout(pv_rect, &view);
                match layout.hit_test(position.x, position.y) {
                    PipelineHit::Action(idx) => {
                        self.last_message =
                            format!("Action on stage {idx}: {}", self.stages[variant][idx].label);
                        self.focused_stage[variant] = Some(idx);
                    }
                    PipelineHit::Body(idx) => {
                        self.last_message =
                            format!("Selected stage {idx}: {}", self.stages[variant][idx].label);
                        self.focused_stage[variant] = Some(idx);
                    }
                    PipelineHit::Empty => return Reaction::Continue,
                }
                Reaction::Redraw
            }
            _ => Reaction::Continue,
        }
    }

    fn source(&self) -> &'static str {
        extract_region(SOURCE)
    }

    fn data(&self, variant: usize) -> serde_json::Value {
        serde_json::to_value(self.view(variant)).unwrap_or(serde_json::Value::Null)
    }

    fn caps_note(&self, _variant: usize, _caps: &BackendCaps) -> Option<String> {
        None
    }
}

Backends

VariantTUIGTKmacOSWindows
In progress
Failed