Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the ViewportHeader widget #2312

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
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