Skip to content
Open
160 changes: 160 additions & 0 deletions druid/examples/viewport_header.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Copyright 2019 The Druid Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Shows a scroll widget, and also demonstrates how widgets that paint
//! outside their bounds can specify their paint region.

// On Windows platform, don't show a console when opening the app.
#![windows_subsystem = "windows"]

use druid::im::Vector;
use druid::widget::prelude::*;
use druid::widget::{Button, Controller, Flex, Label, List, Side, TextBox, ViewportHeader};
use druid::{
AppLauncher, Color, Data, Insets, Lens, LocalizedString, RoundedRectRadii, Selector, WidgetExt,
WindowDesc,
};
use std::sync::Arc;

#[derive(Clone, Data, Lens)]
struct AppData {
list: Vector<Contact>,
count: usize,
}

#[derive(Clone, Data, Lens)]
struct Contact {
name: Arc<String>,
info: Vector<Arc<String>>,
id: usize,
}

pub fn main() {
let window = WindowDesc::new(build_widget())
.title(LocalizedString::new("scroll-demo-window-title").with_placeholder("Scroll demo"));

let mut list = Vector::new();
list.push_back(Arc::new("test".to_string()));
list.push_back(Arc::new("test2".to_string()));
list.push_back(Arc::new("test3".to_string()));

AppLauncher::with_window(window)
.log_to_console()
.launch(AppData {
list: Vector::new(),
count: 0,
})
.expect("launch failed");
}

fn build_widget() -> impl Widget<AppData> {
let list = List::new(|| {
let body = Flex::column()
.with_default_spacer()
.with_child(Label::new("Name:").align_left())
.with_default_spacer()
.with_child(TextBox::new().lens(Contact::name).expand_width())
.with_default_spacer()
.with_default_spacer()
.with_child(Label::new("Info:").align_left())
.with_default_spacer()
.with_child(
List::new(|| {
TextBox::new()
.padding(Insets::new(15.0, 0.0, 0.0, 10.0))
.expand_width()
})
.lens(Contact::info),
)
.with_child(
Button::new("Add Info").on_click(|_, data: &mut Contact, _| {
data.info.push_back(Arc::new(String::new()))
}),
)
.with_default_spacer()
.align_left()
.padding(Insets::uniform_xy(25.0, 0.0))
.background(Color::grey8(25))
.rounded(RoundedRectRadii::new(0.0, 0.0, 10.0, 10.0));

let header = Flex::row()
.with_flex_child(
Label::dynamic(|data: &Contact, _| format!("Contact \"{}\"", &data.name)).center(),
1.0,
)
.with_child(
Button::new("X")
.on_click(|ctx, data: &mut Contact, _| {
ctx.submit_notification(REMOVE_ID.with(data.id))
})
.padding(5.0),
)
.center()
.background(Color::grey8(15))
.rounded(RoundedRectRadii::new(10.0, 10.0, 0.0, 0.0));

ViewportHeader::new(body, header, Side::Top)
.clipped_content(true)
.with_minimum_visible_content(20.0)
.padding(Insets::uniform_xy(0.0, 5.0))
})
.lens(AppData::list)
.controller(RemoveID)
.scroll()
.vertical();

Flex::column()
.with_flex_child(list, 1.0)
.with_default_spacer()
.with_child(
Button::new("Add Contact").on_click(|_, data: &mut AppData, _| {
let name = if data.count == 0 {
"New Contact".to_string()
} else {
format!("New Contact #{}", data.count)
};
let id = data.count;
data.count += 1;
data.list.push_back(Contact {
name: Arc::new(name),
info: Default::default(),
id,
})
}),
)
}

const REMOVE_ID: Selector<usize> = Selector::new("org.druid.example.remove_id");

struct RemoveID;

impl<W: Widget<AppData>> Controller<AppData, W> for RemoveID {
fn event(
&mut self,
child: &mut W,
ctx: &mut EventCtx,
event: &Event,
data: &mut AppData,
env: &Env,
) {
if let Event::Notification(notification) = event {
if let Some(id) = notification.get(REMOVE_ID) {
ctx.set_handled();
data.list.retain(|c| c.id != *id);
}
} else {
child.event(ctx, event, data, env);
}
}
}
41 changes: 41 additions & 0 deletions druid/examples/z_stack_bug.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use druid::widget::{CrossAxisAlignment, Flex, Label, Scroll, SizedBox, ZStack};
use druid::{AppLauncher, Color, UnitPoint, Widget, WidgetExt, WindowDesc};

fn main() {
let window = WindowDesc::new(build_ui());

AppLauncher::with_window(window)
.log_to_console()
.launch(())
.unwrap();
}

fn build_ui() -> impl Widget<()> {
let mut container = Flex::column().cross_axis_alignment(CrossAxisAlignment::Fill);

for _ in 0..10 {
let stack = ZStack::new(
Label::new("Base layer")
.align_vertical(UnitPoint::TOP)
.expand_width()
.fix_height(200.0)
.background(Color::grey8(20)),
)
.with_centered_child(
Label::new("Overlay")
.center()
.fix_height(100.0)
.background(Color::grey8(0)),
);

container.add_child(SizedBox::empty().height(200.0));
container.add_child(
Flex::row()
.with_flex_child(stack, 1.0)
.with_default_spacer()
.with_child(SizedBox::empty()),
);
}

Scroll::new(container).vertical()
}
71 changes: 61 additions & 10 deletions druid/src/widget/align.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

//! A widget that aligns its child (for example, centering it).

use crate::contexts::ChangeCtx;
use crate::debug_state::DebugState;
use crate::widget::prelude::*;
use crate::{Data, Rect, Size, UnitPoint, WidgetPod};
Expand All @@ -25,6 +26,8 @@ pub struct Align<T> {
child: WidgetPod<T, Box<dyn Widget<T>>>,
width_factor: Option<f64>,
height_factor: Option<f64>,
in_viewport: bool,
viewport: Rect,
}

impl<T> Align<T> {
Expand All @@ -39,6 +42,8 @@ impl<T> Align<T> {
child: WidgetPod::new(child).boxed(),
width_factor: None,
height_factor: None,
in_viewport: false,
viewport: Rect::new(0.0, 0.0, f64::INFINITY, f64::INFINITY),
}
}

Expand All @@ -64,6 +69,8 @@ impl<T> Align<T> {
child: WidgetPod::new(child).boxed(),
width_factor: None,
height_factor: Some(1.0),
in_viewport: false,
viewport: Rect::new(0.0, 0.0, f64::INFINITY, f64::INFINITY),
}
}

Expand All @@ -74,8 +81,47 @@ impl<T> Align<T> {
child: WidgetPod::new(child).boxed(),
width_factor: Some(1.0),
height_factor: None,
in_viewport: false,
viewport: Rect::new(0.0, 0.0, f64::INFINITY, f64::INFINITY),
}
}

/// The `Align` widget should only consider the visible space for alignment.
///
/// When the `Align` widget is fully visible, this option has no effect. When the align widget
/// gets scrolled out of view, the wrapped widget will move to stay inside the visible area.
/// The wrapped widget will always stay inside the bounds of the `Align` widget.
pub fn in_viewport(mut self) -> Self {
self.in_viewport = true;
self
}

fn align(&mut self, ctx: &mut impl ChangeCtx, my_size: Size) {
let size = self.child.layout_rect().size();

let extra_width = (my_size.width - size.width).max(0.);
let extra_height = (my_size.height - size.height).max(0.);

// The part of our layout_rect the origin of the child is allowed to be in
let mut extra_space = Rect::new(0., 0., extra_width, extra_height);

if self.in_viewport {
// The part of the viewport the origin of the child is allowed to be in
let viewport =
Rect::from_origin_size(self.viewport.origin(), self.viewport.size() - size);

// Essentially Rect::intersect but if the two rectangles dont intersect this
// implementation chooses the point closed to viewpor inside extra_space to always give
// the child a valid origin.
extra_space.x0 = extra_space.x0.clamp(viewport.x0, extra_space.x1);
extra_space.y0 = extra_space.y0.clamp(viewport.y0, extra_space.y1);
extra_space.x1 = extra_space.x1.clamp(extra_space.x0, viewport.x1);
extra_space.y1 = extra_space.y1.clamp(extra_space.y0, viewport.y1);
}

let origin = self.align.resolve(extra_space).expand();
self.child.set_origin(ctx, origin);
}
}

impl<T: Data> Widget<T> for Align<T> {
Expand All @@ -86,6 +132,14 @@ impl<T: Data> Widget<T> for Align<T> {

#[instrument(name = "Align", level = "trace", skip(self, ctx, event, data, env))]
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) {
// THis needs to happen before passing the event to the child.
if let LifeCycle::ViewContextChanged(view_ctx) = event {
self.viewport = view_ctx.clip;
if self.in_viewport {
self.align(ctx, ctx.size());
}
}

self.child.lifecycle(ctx, event, data, env)
}

Expand Down Expand Up @@ -118,27 +172,24 @@ impl<T: Data> Widget<T> for Align<T> {
my_size.height = size.height * height;
}

my_size = bc.constrain(my_size);
let extra_width = (my_size.width - size.width).max(0.);
let extra_height = (my_size.height - size.height).max(0.);
let origin = self
.align
.resolve(Rect::new(0., 0., extra_width, extra_height))
.expand();
self.child.set_origin(ctx, origin);
let my_size = bc.constrain(my_size);
self.align(ctx, my_size);

let my_insets = self.child.compute_parent_paint_insets(my_size);
ctx.set_paint_insets(my_insets);

if self.height_factor.is_some() {
let baseline_offset = self.child.baseline_offset();
if baseline_offset > 0f64 {
ctx.set_baseline_offset(baseline_offset + extra_height / 2.0);
ctx.set_baseline_offset(
my_size.height - self.child.layout_rect().y1 + baseline_offset,
);
}
}

trace!(
"Computed layout: origin={}, size={}, insets={:?}",
origin,
self.child.layout_rect().origin(),
my_size,
my_insets
);
Expand Down
Loading