New Top Bar (#7488)

Fixes #7411

So far, this branch removes window control buttons and go-to dashboard (hamburger icon), and adds option for dashboard to set offset of the rest of top bar panels.
This commit is contained in:
Adam Obuchowicz 2023-08-08 17:23:41 +02:00 committed by GitHub
parent b656b336c7
commit 59329bd59a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 240 additions and 745 deletions

View File

@ -268,10 +268,6 @@ pub fn register_views(app: &Application) {
// ListView we use below.
type PlaceholderEntryType = ensogl_component::list_view::entry::Label;
app.views.register::<ensogl_component::list_view::ListView<PlaceholderEntryType>>();
if enso_config::ARGS.groups.startup.options.platform.value == "web" {
app.views.register::<ide_view::project_view_top_bar::window_control_buttons::View>();
}
}

View File

@ -1,95 +0,0 @@
//! Provides a button to switch back from the project view to the dashboard.
use ensogl::prelude::*;
use ensogl::application::tooltip;
use ensogl::application::Application;
use ensogl::display;
use ensogl::display::style;
use ensogl_component::toggle_button;
use ensogl_component::toggle_button::ToggleButton;
// =================
// === Constants ===
// =================
/// The width and height of the button.
pub const SIZE: f32 = 16.0;
// ============
// === Icon ===
// ============
/// Defines an icon for returning to the dashboard. It looks like a hamburger button.
mod icon {
use super::*;
use ensogl::data::color;
use ensogl_component::toggle_button::ColorableShape;
ensogl::shape! {
(style: Style, color_rgba: Vector4<f32>) {
let fill_color = Var::<color::Rgba>::from(color_rgba);
let width = Var::<Pixels>::from("input_size.x");
let height = Var::<Pixels>::from("input_size.y");
let unit = &width / SIZE;
let mid_bar = Rect((&unit * 12.0, &unit * 3.0)).corners_radius(&unit);
let top_bar = mid_bar.translate_y(&unit * -5.0);
let bottom_bar = mid_bar.translate_y(&unit * 5.0);
let all_bars = top_bar + mid_bar + bottom_bar;
let shape = all_bars.fill(fill_color);
let hover_area = Rect((&width, &height)).fill(INVISIBLE_HOVER_COLOR);
(shape + hover_area).into()
}
}
impl ColorableShape for Shape {
fn set_color(&self, color: color::Rgba) {
self.color_rgba.set(Vector4::new(color.red, color.green, color.blue, color.alpha));
}
}
}
// ============
// === View ===
// ============
/// Provides a button to switch back from the project view to the dashboard.
#[derive(Debug, Clone, CloneRef, Deref, display::Object)]
pub struct View {
button: ToggleButton<icon::Shape>,
}
impl View {
/// Constructor.
pub fn new(app: &Application) -> Self {
let scene = &app.display.default_scene;
let tooltip_style = tooltip::Style::set_label("Dashboard".to_owned())
.with_placement(tooltip::Placement::Right);
let button = ToggleButton::<icon::Shape>::new(app, tooltip_style);
scene.layers.panel.add(&button);
button.set_color_scheme(Self::color_scheme(&scene.style_sheet));
button.set_size(Vector2(SIZE, SIZE));
Self { button }
}
fn color_scheme(style_sheet: &style::Sheet) -> toggle_button::ColorScheme {
let default_color_scheme = toggle_button::default_color_scheme(style_sheet);
toggle_button::ColorScheme {
// Make it look like a normal button (as opposed to a toggle button) by not having a
// toggled state visually.
toggled: default_color_scheme.non_toggled,
toggled_hovered: default_color_scheme.hovered,
..default_color_scheme
}
}
}

View File

@ -18,7 +18,6 @@
use ensogl::prelude::*;
use enso_config::ARGS;
use ensogl::application::Application;
use ensogl::display;
use ensogl::display::shape::compound::rectangle::Rectangle;
@ -32,14 +31,10 @@ use project_name::ProjectName;
// ==============
pub mod project_name;
pub mod window_control_buttons;
pub use breadcrumbs::LocalCall;
mod breadcrumbs;
mod go_to_dashboard_button;
@ -116,9 +111,6 @@ impl ProjectNameWithEnvironmentSelector {
pub struct ProjectViewTopBar {
#[display_object]
root: display::object::Instance,
/// These buttons are only visible in a cloud environment.
pub window_control_buttons: window_control_buttons::View,
pub go_to_dashboard_button: go_to_dashboard_button::View,
pub breadcrumbs: breadcrumbs::Breadcrumbs,
pub project_name_with_environment_selector: ProjectNameWithEnvironmentSelector,
network: frp::Network,
@ -128,15 +120,9 @@ impl ProjectViewTopBar {
/// Constructor.
pub fn new(app: &Application) -> Self {
let root = display::object::Instance::new_named("ProjectViewTopBar");
let window_control_buttons = app.new_view::<window_control_buttons::View>();
let go_to_dashboard_button = go_to_dashboard_button::View::new(app);
let breadcrumbs = breadcrumbs::Breadcrumbs::new(app);
let project_name_with_environment_selector = ProjectNameWithEnvironmentSelector::new(app);
if ARGS.groups.startup.options.platform.value == "web" {
root.add_child(&window_control_buttons);
}
root.add_child(&go_to_dashboard_button);
root.add_child(&project_name_with_environment_selector);
root.add_child(&breadcrumbs);
root.use_auto_layout().set_children_alignment_center();
@ -145,15 +131,7 @@ impl ProjectViewTopBar {
let network = frp::Network::new("ProjectViewTopBar");
Self {
root,
window_control_buttons,
go_to_dashboard_button,
breadcrumbs,
project_name_with_environment_selector,
network,
}
.init()
Self { root, breadcrumbs, project_name_with_environment_selector, network }.init()
}
fn init(self) -> Self {
@ -165,10 +143,13 @@ impl ProjectViewTopBar {
init <- source_();
let gap = style_watch.get_number(theme::gap);
let padding_left = style_watch.get_number(theme::padding_left);
let padding_top = style_watch.get_number(theme::padding_top);
gap <- all(init, gap)._1();
padding_left <- all(init, padding_left)._1();
padding_top <- all(init, padding_top)._1();
eval gap([root](g) { root.set_gap((*g, 0.0)); });
eval padding_left([root](p) { root.set_padding_left(*p); });
eval padding_top([root](p) { root.set_padding_top(*p); });
}
init.emit(());
self

View File

@ -1,207 +0,0 @@
//! The component with buttons in the top left corner. See [[View]].
use ensogl::display::shape::*;
use ensogl::prelude::*;
use enso_frp as frp;
use ensogl::application;
use ensogl::application::Application;
use ensogl::display;
use ensogl::display::object::ObjectOps;
use ensogl::shape;
use ensogl_hardcoded_theme::application::window_control_buttons as theme;
// =================
// === Constants ===
// =================
/// Width of the buttons panel.
pub const MACOS_TRAFFIC_LIGHTS_CONTENT_WIDTH: f32 = 52.0;
/// Height of the buttons panel.
pub const MACOS_TRAFFIC_LIGHTS_CONTENT_HEIGHT: f32 = 12.0;
/// Horizontal and vertical offset between traffic lights and window border
pub const MACOS_TRAFFIC_LIGHTS_SIDE_OFFSET: f32 = 13.0;
/// The vertical center of the traffic lights, relative to the window border.
pub const MACOS_TRAFFIC_LIGHTS_VERTICAL_CENTER: f32 =
-MACOS_TRAFFIC_LIGHTS_SIDE_OFFSET - MACOS_TRAFFIC_LIGHTS_CONTENT_HEIGHT / 2.0;
// ==============
// === Export ===
// ==============
pub mod close;
pub mod fullscreen;
// ==============
// === Shapes ===
// ==============
mod shape {
use super::*;
shape! {
alignment = center;
(style: Style) {
Plane().fill(INVISIBLE_HOVER_COLOR).into()
}
}
}
// =============
// === Model ===
// =============
/// An internal model of Status Bar component
#[derive(Clone, CloneRef, Debug, display::Object)]
pub struct Model {
display_object: display::object::Instance,
shape: shape::View,
close: close::View,
fullscreen: fullscreen::View,
}
impl Model {
/// Constructor.
pub fn new(app: &Application) -> Self {
let display_object = display::object::Instance::new_named("WindowControlButtons");
ensogl::shapes_order_dependencies! {
app.display.default_scene => {
shape -> close::shape;
shape -> fullscreen::shape;
}
};
let close = close::View::new(app);
display_object.add_child(&close);
let fullscreen = fullscreen::View::new(app);
display_object.add_child(&fullscreen);
let shape = shape::View::new();
display_object.add_child(&shape);
app.display.default_scene.layers.panel.add(&display_object);
Self { display_object, shape, close, fullscreen }
}
/// Updates positions of the buttons and sizes of the mouse area.
pub fn set_layout(&self, spacing: f32) {
let close_size = self.close.size.value();
let fullscreen_size = self.fullscreen.size.value();
let fullscreen_offset = Vector2(close_size.x + spacing, 0.0);
self.fullscreen.set_xy(fullscreen_offset);
let width = fullscreen_offset.x + fullscreen_size.x;
let height = max(close_size.y, fullscreen_size.y);
let size = Vector2(width, height);
self.shape.set_size(size);
self.display_object.set_size(size);
}
}
// ===========
// === FRP ===
// ===========
ensogl::define_endpoints! {
Input {
enabled (bool),
}
Output {
close(),
fullscreen(),
}
}
// ============
// === View ===
// ============
/// The Top Buttons Panel component.
///
/// The panel contains two buttons: one for closing IDE and one for toggling the fullscreen mode.
/// The panel is meant to be displayed only when IDE runs in a cloud environment.
#[derive(Clone, CloneRef, Debug, display::Object)]
pub struct View {
#[allow(missing_docs)]
pub frp: Frp,
#[display_object]
model: Model,
style: StyleWatchFrp,
}
impl View {
/// Constructor.
pub fn new(app: &Application) -> Self {
let frp = Frp::new();
let model = Model::new(app);
let network = &frp.network;
let style = StyleWatchFrp::new(&app.display.default_scene.style_sheet);
let radius = style.get_number(theme::radius);
let spacing = style.get_number(theme::spacing);
frp::extend! { network
// Layout
button_size <- radius.map(|&r| Vector2(2.0 * r, 2.0 * r));
model.close.set_size <+ button_size;
model.fullscreen.set_size <+ button_size;
button_resized <- any_(&model.close.size, &model.fullscreen.size);
_eval <- all_with(&button_resized, &spacing,
f!((_, spacing) model.set_layout(*spacing))
);
// Handle the panel-wide hover
mouse_near_buttons <- bool(
&model.shape.events_deprecated.mouse_out,
&model.shape.events_deprecated.mouse_over
);
mouse_on_any_buttton <- model.close.is_hovered.or(&model.fullscreen.is_hovered);
mouse_nearby <- mouse_near_buttons.or(&mouse_on_any_buttton);
model.close.mouse_nearby <+ mouse_nearby;
model.fullscreen.mouse_nearby <+ mouse_nearby;
// === Handle buttons' clicked events ===
frp.source.close <+ model.close.clicked;
frp.source.fullscreen <+ model.fullscreen.clicked;
}
model.set_layout(spacing.value());
Self { frp, model, style }
}
}
impl Deref for View {
type Target = Frp;
fn deref(&self) -> &Self::Target {
&self.frp
}
}
impl FrpNetworkProvider for View {
fn network(&self) -> &frp::Network {
&self.frp.network
}
}
impl application::View for View {
fn label() -> &'static str {
"TopButtons"
}
fn new(app: &Application) -> Self {
View::new(app)
}
}

View File

@ -1,78 +0,0 @@
//! The close button in the Top Button panel.
use ensogl_component::button::prelude::*;
// ==============
// === Export ===
// ==============
pub use ensogl_hardcoded_theme::application::window_control_buttons::close as theme;
// =============
// === Shape ===
// =============
/// The shape for "close" button. It places X-lie cross on a circle.
pub mod shape {
use super::*;
ensogl::shape! {
(style: Style, background_color: Vector4<f32>, icon_color: Vector4<f32>) {
let size = Var::canvas_size();
let radius = Min::min(size.x(),size.y()) / 2.0;
let angle = Radians::from(45.0.degrees());
let bar_length = &radius * 4.0 / 3.0;
let bar_width = &bar_length / 6.5;
#[allow(clippy::disallowed_names)] // The `bar` name here is totally legit.
let bar = Rect((bar_length, &bar_width)).corners_radius(bar_width);
let cross = (bar.rotate(angle) + bar.rotate(-angle)).into();
shape(background_color, icon_color, cross, radius)
}
}
}
impl ButtonShape for shape::Shape {
fn debug_name() -> &'static str {
"CloseButton"
}
fn background_color_path(state: State) -> StaticPath {
match state {
State::Unconcerned => theme::normal::background_color,
State::Hovered => theme::hovered::background_color,
State::Pressed => theme::pressed::background_color,
}
}
fn icon_color_path(state: State) -> StaticPath {
match state {
State::Unconcerned => theme::normal::icon_color,
State::Hovered => theme::hovered::icon_color,
State::Pressed => theme::pressed::icon_color,
}
}
fn background_color(&self) -> &ProxyParam<Attribute<Vector4<f32>>> {
&self.background_color
}
fn icon_color(&self) -> &ProxyParam<Attribute<Vector4<f32>>> {
&self.icon_color
}
}
// ============
// === View ===
// ============
/// The view component with the close button.
///
/// The button styled after macOS, i.e. consists of an icon shape placed on top of a circle.
/// The icon is visible when button or its neighborhood (as provided by `mouse_nearby` input) is
/// hovered.
pub type View = ensogl_component::button::View<shape::Shape>;

View File

@ -1,77 +0,0 @@
//! The fullscreen button in the Top Button panel.
use ensogl_component::button::prelude::*;
// ==============
// === Export ===
// ==============
pub use ensogl_hardcoded_theme::application::window_control_buttons::fullscreen as theme;
// =============
// === Shape ===
// =============
/// The shape for "fullscreen" button. The icon consists if two triangles ◤◢ centered around single
/// point.
pub mod shape {
use super::*;
ensogl::shape! {
(style: Style, background_color: Vector4<f32>, icon_color: Vector4<f32>) {
let size = Var::canvas_size();
let radius = Min::min(size.x(),size.y()) / 2.0;
let round = &radius / 6.0;
let rect = Rect((&radius,&radius)).corners_radius(round);
let strip_sizes = (&radius * 2.0 / 9.0, &radius*2.0);
let strip = Rect(strip_sizes).rotate(Radians::from(45.0.degrees()));
let icon = rect - strip;
shape(background_color, icon_color, icon.into(), radius)
}
}
}
impl ButtonShape for shape::Shape {
fn debug_name() -> &'static str {
"FullscreenButton"
}
fn background_color_path(state: State) -> StaticPath {
match state {
State::Unconcerned => theme::normal::background_color,
State::Hovered => theme::hovered::background_color,
State::Pressed => theme::pressed::background_color,
}
}
fn icon_color_path(state: State) -> StaticPath {
match state {
State::Unconcerned => theme::normal::icon_color,
State::Hovered => theme::hovered::icon_color,
State::Pressed => theme::pressed::icon_color,
}
}
fn background_color(&self) -> &ProxyParam<Attribute<Vector4<f32>>> {
&self.background_color
}
fn icon_color(&self) -> &ProxyParam<Attribute<Vector4<f32>>> {
&self.icon_color
}
}
// ============
// === View ===
// ============
/// The view component with the fullscreen button.
///
/// The button styled after macOS, i.e. consists of an icon shape placed on top of a circle.
/// The icon is visible when button or its neighborhood (as provided by `mouse_nearby` input) is
/// hovered.
pub type View = ensogl_component::button::View<shape::Shape>;

View File

@ -26,7 +26,6 @@ use ensogl_component::text;
use ensogl_component::text::selection::Selection;
use ensogl_hardcoded_theme::Theme;
use ide_view_graph_editor::NodeSource;
use ide_view_project_view_top_bar::window_control_buttons;
use ide_view_project_view_top_bar::ProjectViewTopBar;
@ -285,20 +284,12 @@ impl Model {
project_view_top_bar_size: Vector2,
) {
let top_left = Vector2(-scene_shape.width, scene_shape.height) / 2.0;
let buttons_y = window_control_buttons::MACOS_TRAFFIC_LIGHTS_VERTICAL_CENTER;
let y = buttons_y - project_view_top_bar_size.y / 2.0;
let project_view_top_bar_origin = Vector2(0.0, y);
let y = -project_view_top_bar_size.y;
let x = ARGS.groups.window.options.top_bar_offset.value;
let project_view_top_bar_origin = Vector2(x as f32, y);
self.top_bar.set_xy(top_left + project_view_top_bar_origin);
}
fn on_close_clicked(&self) {
js::close(enso_config::window_app_scope_name);
}
fn on_fullscreen_clicked(&self) {
js::fullscreen();
}
fn show_project_list(&self) {
self.display_object.add_child(&*self.project_list);
}
@ -318,43 +309,6 @@ impl Model {
mod js {
// use super::*;
use wasm_bindgen::prelude::*;
#[wasm_bindgen(inline_js = "
export function close(windowAppScopeConfigName) {
try { window[windowAppScopeConfigName].close(); }
catch(e) {
console.error(`Exception thrown from window.${windowAppScopeConfigName}.close:`,e)
}
}")]
extern "C" {
#[allow(unsafe_code)]
pub fn close(window_app_scope_name: &str);
}
#[wasm_bindgen(inline_js = "
export function fullscreen() {
try {
if(document.fullscreenElement === null)
document.documentElement.requestFullscreen()
else
document.exitFullscreen()
} catch (e) {
console.error('Exception thrown when toggling fullscreen display mode:',e)
}
}
")]
extern "C" {
#[allow(unsafe_code)]
pub fn fullscreen();
}
}
// ============
// === View ===
// ============
@ -424,12 +378,6 @@ impl View {
let project_view_top_bar = &model.top_bar;
frp::extend! { network
init <- source_();
let window_control_buttons = &project_view_top_bar.window_control_buttons;
eval_ window_control_buttons.close (model.on_close_clicked());
eval_ window_control_buttons.fullscreen (model.on_fullscreen_clicked());
let go_to_dashboard_button = &project_view_top_bar.go_to_dashboard_button;
frp.source.go_to_dashboard_button_pressed <+
go_to_dashboard_button.is_pressed.on_true();
let project_view_top_bar_display_object = project_view_top_bar.display_object();
_eval <- all_with3(

View File

@ -1,67 +0,0 @@
//! Defines a UI container for the window control buttons and the "go to dashboard" button. This is
//! merely here to make use of the auto-layout functionality.
use ensogl::prelude::*;
use enso_config::ARGS;
use ensogl::application::Application;
use ensogl::display;
mod go_to_dashboard_button;
pub mod window_control_buttons;
// =================
// === Constants ===
// =================
/// The gap in pixels between the various components of the project view top bar.
const GAP: f32 = 16.0;
/// The padding left of the project view top bar.
const PADDING_LEFT: f32 = 19.0;
// ============================
// === Project View Top Bar ===
// ============================
/// Defines a UI container for the window control buttons and the "go to dashboard" button. This is
/// merely here to make use of the auto-layout functionality.
#[derive(Clone, CloneRef, Debug, display::Object)]
#[allow(missing_docs)]
pub struct ProjectViewTopBar {
display_object: display::object::Instance,
/// These buttons are only visible in a cloud environment.
pub window_control_buttons: window_control_buttons::View,
pub go_to_dashboard_button: go_to_dashboard_button::View,
}
impl ProjectViewTopBar {
/// Constructor.
pub fn new(app: &Application) -> Self {
let display_object = display::object::Instance::new_named("ProjectViewTopBar");
let window_control_buttons = app.new_view::<window_control_buttons::View>();
let go_to_dashboard_button = go_to_dashboard_button::View::new(app);
if ARGS.groups.startup.options.platform.value == "web" {
display_object.add_child(&window_control_buttons);
}
display_object.add_child(&go_to_dashboard_button);
display_object
.use_auto_layout()
.set_gap((GAP, 0.0))
.set_padding_left(PADDING_LEFT)
// We use `GAP` as the right padding since it delimits the space to the part of the top
// bar that's defined in the graph editor.
.set_padding_right(GAP)
.set_children_alignment_center();
app.display.default_scene.layers.panel.add(&display_object);
Self { display_object, window_control_buttons, go_to_dashboard_button }
}
}

View File

@ -24,7 +24,7 @@ export enum Platform {
linux = 'Linux',
}
/** Returns the platform the app is currently running on.
/** Return the platform the app is currently running on.
* This is used to determine whether `metaKey` or `ctrlKey` is used in shortcuts. */
export function platform(): Platform {
if (/windows/i.test(navigator.userAgent)) {
@ -37,3 +37,23 @@ export function platform(): Platform {
return Platform.unknown
}
}
/** Return whether the device is running Windows. */
export function isOnWindows() {
return platform() === Platform.windows
}
/** Return whether the device is running macOS. */
export function isOnMacOS() {
return platform() === Platform.macOS
}
/** Return whether the device is running Linux. */
export function isOnLinux() {
return platform() === Platform.linux
}
/** Return whether the device is running an unknown OS. */
export function isOnUnknownOS() {
return platform() === Platform.unknown
}

View File

@ -28,6 +28,11 @@
"value": true,
"defaultDescription": "false on MacOS, true otherwise",
"description": "Draw window frame."
},
"topBarOffset": {
"value": 0,
"description": "The offset of rust-rendered toolbar from window's left edge.",
"primary": false
}
}
},

View File

@ -17,7 +17,7 @@ export interface AssetInfoBarProps {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default function AssetInfoBar(_props: AssetInfoBarProps) {
return (
<div className="flex items-center shrink-0 bg-frame-bg rounded-full gap-3 h-8 px-2">
<div className="flex items-center shrink-0 bg-frame-bg rounded-full gap-3 h-8 px-2 cursor-default pointer-events-auto">
<Button
active={false}
disabled

View File

@ -8,12 +8,13 @@ export interface ButtonProps {
image: string
/** A title that is only shown when `disabled` is true. */
error?: string | null
className?: string
onClick: (event: React.MouseEvent) => void
}
/** A styled button. */
export default function Button(props: ButtonProps) {
const { active = false, disabled = false, image, error, onClick } = props
const { active = false, disabled = false, image, error, className, onClick } = props
return (
<button
@ -21,7 +22,7 @@ export default function Button(props: ButtonProps) {
{...(disabled && error != null ? { title: error } : {})}
className={`cursor-pointer disabled:cursor-default disabled:opacity-50 disabled:cursor-not-allowed hover:opacity-100 ${
active ? '' : 'opacity-50'
}`}
} ${className ?? ''}`}
onClick={onClick}
>
<img src={image} />

View File

@ -77,6 +77,18 @@ export default function Dashboard(props: DashboardProps) {
unsetModal()
}, [page, /* should never change */ unsetModal])
React.useEffect(() => {
const onClick = () => {
if (getSelection()?.type !== 'Range') {
unsetModal()
}
}
document.addEventListener('click', onClick)
return () => {
document.removeEventListener('click', onClick)
}
}, [/* should never change */ unsetModal])
React.useEffect(() => {
if (
supportsLocalBackend &&
@ -91,16 +103,6 @@ export default function Dashboard(props: DashboardProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
React.useEffect(() => {
const goToDrive = () => {
setPage(pageSwitcher.Page.drive)
}
document.addEventListener('show-dashboard', goToDrive)
return () => {
document.removeEventListener('show-dashboard', goToDrive)
}
}, [])
React.useEffect(() => {
// The types come from a third-party API and cannot be changed.
// eslint-disable-next-line no-restricted-syntax
@ -206,101 +208,113 @@ export default function Dashboard(props: DashboardProps) {
setProject(null)
}, [])
const closeModalIfExists = React.useCallback(() => {
if (getSelection()?.type !== 'Range') {
unsetModal()
}
}, [/* should never change */ unsetModal])
const driveHiddenClass = page === pageSwitcher.Page.drive ? '' : 'hidden'
return (
<div
className={`flex flex-col gap-2 relative select-none text-primary text-xs h-screen pb-2 ${
page === pageSwitcher.Page.drive ? '' : 'hidden'
}`}
onContextMenu={event => {
event.preventDefault()
unsetModal()
}}
onClick={closeModalIfExists}
>
<TopBar
supportsLocalBackend={supportsLocalBackend}
projectName={project?.name ?? null}
page={page}
setPage={setPage}
asset={null}
isEditorDisabled={project == null}
isHelpChatOpen={isHelpChatOpen}
setIsHelpChatOpen={setIsHelpChatOpen}
setBackendType={setBackendType}
query={query}
setQuery={setQuery}
/>
{isListingRemoteDirectoryWhileOffline ? (
<div className="grow grid place-items-center mx-2">
<div className="flex flex-col gap-4">
<div className="text-base text-center">You are not signed in.</div>
<button
className="text-base text-white bg-help rounded-full self-center leading-170 h-8 py-px w-16"
onClick={() => {
navigate(app.LOGIN_PATH)
}}
>
Login
</button>
</div>
</div>
) : isListingLocalDirectoryAndWillFail ? (
<div className="grow grid place-items-center mx-2">
<div className="text-base text-center">
Could not connect to the Project Manager. Please try restarting{' '}
{common.PRODUCT_NAME}, or manually launching the Project Manager.
</div>
</div>
) : isListingRemoteDirectoryAndWillFail ? (
<div className="grow grid place-items-center mx-2">
<div className="text-base text-center">
We will review your user details and enable the cloud experience for you
shortly.
</div>
</div>
) : (
<>
<Templates onTemplateClick={doCreateProject} />
<DriveView
page={page}
initialProjectName={initialProjectName}
directoryId={directoryId}
setDirectoryId={setDirectoryId}
assetListEvents={assetListEvents}
dispatchAssetListEvent={dispatchAssetListEvent}
query={query}
doCreateProject={doCreateProject}
doOpenEditor={openEditor}
doCloseEditor={closeEditor}
appRunner={appRunner}
loadingProjectManagerDidFail={loadingProjectManagerDidFail}
isListingRemoteDirectoryWhileOffline={isListingRemoteDirectoryWhileOffline}
isListingLocalDirectoryAndWillFail={isListingLocalDirectoryAndWillFail}
isListingRemoteDirectoryAndWillFail={isListingRemoteDirectoryAndWillFail}
/>
</>
)}
<TheModal />
<Editor
visible={page === pageSwitcher.Page.editor}
project={project}
appRunner={appRunner}
/>
{/* `session.accessToken` MUST be present in order for the `Chat` component to work. */}
{isHelpChatVisible && session.accessToken != null && (
<Chat
isOpen={isHelpChatOpen}
doClose={() => {
setIsHelpChatOpen(false)
<>
<div
className={`flex flex-col gap-2 relative select-none text-primary text-xs h-screen pb-2 ${
page === pageSwitcher.Page.editor ? 'cursor-none pointer-events-none' : ''
}`}
onContextMenu={event => {
event.preventDefault()
unsetModal()
}}
>
<TopBar
supportsLocalBackend={supportsLocalBackend}
projectName={project?.name ?? null}
page={page}
setPage={setPage}
asset={null}
isEditorDisabled={project == null}
isHelpChatOpen={isHelpChatOpen}
setIsHelpChatOpen={setIsHelpChatOpen}
setBackendType={setBackendType}
query={query}
setQuery={setQuery}
onSignOut={() => {
if (page === pageSwitcher.Page.editor) {
setPage(pageSwitcher.Page.drive)
}
setProject(null)
}}
/>
)}
</div>
{isListingRemoteDirectoryWhileOffline ? (
<div className={`grow grid place-items-center mx-2 ${driveHiddenClass}`}>
<div className="flex flex-col gap-4">
<div className="text-base text-center">You are not signed in.</div>
<button
className="text-base text-white bg-help rounded-full self-center leading-170 h-8 py-px w-16"
onClick={() => {
navigate(app.LOGIN_PATH)
}}
>
Login
</button>
</div>
</div>
) : isListingLocalDirectoryAndWillFail ? (
<div className={`grow grid place-items-center mx-2 ${driveHiddenClass}`}>
<div className="text-base text-center">
Could not connect to the Project Manager. Please try restarting{' '}
{common.PRODUCT_NAME}, or manually launching the Project Manager.
</div>
</div>
) : isListingRemoteDirectoryAndWillFail ? (
<div className={`grow grid place-items-center mx-2 ${driveHiddenClass}`}>
<div className="text-base text-center">
We will review your user details and enable the cloud experience for you
shortly.
</div>
</div>
) : (
<>
<Templates
hidden={page !== pageSwitcher.Page.drive}
onTemplateClick={doCreateProject}
/>
<DriveView
hidden={page !== pageSwitcher.Page.drive}
page={page}
initialProjectName={initialProjectName}
directoryId={directoryId}
setDirectoryId={setDirectoryId}
assetListEvents={assetListEvents}
dispatchAssetListEvent={dispatchAssetListEvent}
query={query}
doCreateProject={doCreateProject}
doOpenEditor={openEditor}
doCloseEditor={closeEditor}
appRunner={appRunner}
loadingProjectManagerDidFail={loadingProjectManagerDidFail}
isListingRemoteDirectoryWhileOffline={
isListingRemoteDirectoryWhileOffline
}
isListingLocalDirectoryAndWillFail={isListingLocalDirectoryAndWillFail}
isListingRemoteDirectoryAndWillFail={
isListingRemoteDirectoryAndWillFail
}
/>
</>
)}
<Editor
visible={page === pageSwitcher.Page.editor}
project={project}
appRunner={appRunner}
/>
{/* `session.accessToken` MUST be present in order for the `Chat` component to work. */}
{isHelpChatVisible && session.accessToken != null && (
<Chat
isOpen={isHelpChatOpen}
doClose={() => {
setIsHelpChatOpen(false)
}}
/>
)}
</div>
<div className="text-xs text-primary select-none">
<TheModal />
</div>
</>
)
}

View File

@ -31,6 +31,7 @@ const DIRECTORY_STACK_KEY = `${common.PRODUCT_NAME.toLowerCase()}-dashboard-dire
/** Props for a {@link DriveView}. */
export interface DriveViewProps {
page: pageSwitcher.Page
hidden: boolean
initialProjectName: string | null
directoryId: backendModule.DirectoryId | null
setDirectoryId: (directoryId: backendModule.DirectoryId) => void
@ -51,6 +52,7 @@ export interface DriveViewProps {
export default function DriveView(props: DriveViewProps) {
const {
page,
hidden,
initialProjectName,
directoryId,
setDirectoryId,
@ -255,7 +257,11 @@ export default function DriveView(props: DriveViewProps) {
}, [page])
return (
<div className="flex flex-col flex-1 overflow-hidden gap-2.5 px-3.25">
<div
className={`flex flex-col flex-1 overflow-hidden gap-2.5 px-3.25 ${
hidden ? 'hidden' : ''
}`}
>
<div className="flex flex-col self-start gap-3">
<h1 className="text-xl font-bold h-9.5 pl-1.5">
{backend.type === backendModule.BackendType.remote

View File

@ -11,6 +11,8 @@ import GLOBAL_CONFIG from '../../../../../../../../gui/config.yaml' assert { typ
// === Constants ===
// =================
/** The horizontal offset of the editor's top bar from the left edge of the window. */
const TOP_BAR_X_OFFSET_PX = 96
/** The `id` attribute of the element into which the IDE will be rendered. */
const IDE_ELEMENT_ID = 'root'
const IDE_CDN_URL = 'https://cdn.enso.org/ide'
@ -125,6 +127,9 @@ export default function Editor(props: EditorProps) {
startup: {
project: project.packageName,
},
window: {
topBarOffset: `${TOP_BAR_X_OFFSET_PX}`,
},
},
// Here we actually need explicit undefined.
// eslint-disable-next-line no-restricted-syntax
@ -163,9 +168,6 @@ export default function Editor(props: EditorProps) {
return () => {
appRunner.stopApp()
}
// The backend MUST NOT be a dependency, since the IDE should only be recreated when a new
// project is opened, and a local project does not exist on the cloud and vice versa.
// eslint-disable-next-line react-hooks/exhaustive-deps
}
}, [project, /* should never change */ appRunner])

View File

@ -23,7 +23,7 @@ export default function Modal(props: ModalProps) {
return (
<div
className={`inset-0 bg-primary ${
className={`inset-0 bg-primary z-10 ${
centered ? 'fixed w-screen h-screen grid place-items-center ' : ''
}${className ?? ''}`}
onClick={event => {

View File

@ -60,6 +60,7 @@ export default function PageSwitcher(props: PageSwitcherProps) {
active={page === pageData.page}
disabled={isDisabled}
error={ERRORS[pageData.page]}
className="pointer-events-auto"
onClick={() => {
setPage(pageData.page)
}}

View File

@ -242,6 +242,7 @@ function TemplatesRender(props: InternalTemplatesRenderProps) {
/** Props for a {@link Templates}. */
export interface TemplatesProps {
hidden: boolean
onTemplateClick: (
name: string | null,
onSpinnerStateChange: (state: spinner.SpinnerState | null) => void
@ -250,7 +251,7 @@ export interface TemplatesProps {
/** A container for a {@link TemplatesRender} which passes it a list of templates. */
export default function Templates(props: TemplatesProps) {
const { onTemplateClick } = props
const { hidden, onTemplateClick } = props
const [shadowClass, setShadowClass] = React.useState(
window.innerWidth <= MAX_WIDTH_NEEDING_SCROLL ? ShadowClass.bottom : ShadowClass.none
@ -316,7 +317,7 @@ export default function Templates(props: TemplatesProps) {
}, [isOpen])
return (
<div className="mx-2">
<div className={`mx-2 ${hidden ? 'hidden' : ''}`}>
<div className="flex items-center my-2">
<div className="w-4">
<div

View File

@ -28,6 +28,7 @@ export interface TopBarProps {
setIsHelpChatOpen: (isHelpChatOpen: boolean) => void
query: string
setQuery: (value: string) => void
onSignOut: () => void
}
/** The {@link TopBarProps.setQuery} parameter is used to communicate with the parent component,
@ -44,33 +45,44 @@ export default function TopBar(props: TopBarProps) {
setIsHelpChatOpen,
query,
setQuery,
onSignOut,
} = props
return (
<div className="relative flex ml-4.75 mr-2.25 mt-2.25 h-8 gap-6">
<div className="relative flex ml-4.75 mr-2.25 mt-2.25 h-8 gap-6 z-10">
<PageSwitcher page={page} setPage={setPage} isEditorDisabled={isEditorDisabled} />
{supportsLocalBackend && <BackendSwitcher setBackendType={setBackendType} />}
<div className="grow" />
<div className="search-bar absolute flex items-center text-primary bg-frame-bg rounded-full -translate-x-1/2 gap-2.5 left-1/2 h-8 w-98.25 px-2">
<label htmlFor="search">
<img src={FindIcon} className="opacity-80" />
</label>
<input
type="text"
size={1}
id="search"
placeholder="Type to search for projects, data connectors, users, and more."
value={query}
onChange={event => {
setQuery(event.target.value)
}}
className="grow bg-transparent leading-5 h-6 py-px"
/>
</div>
{supportsLocalBackend && page !== pageSwitcher.Page.editor && (
<BackendSwitcher setBackendType={setBackendType} />
)}
<div className="grow" />
{page !== pageSwitcher.Page.editor && (
<>
<div className="search-bar absolute flex items-center text-primary bg-frame-bg rounded-full -translate-x-1/2 gap-2.5 left-1/2 h-8 w-98.25 px-2">
<label htmlFor="search">
<img src={FindIcon} className="opacity-80" />
</label>
<input
type="text"
size={1}
id="search"
placeholder="Type to search for projects, data connectors, users, and more."
value={query}
onChange={event => {
setQuery(event.target.value)
}}
className="grow bg-transparent leading-5 h-6 py-px"
/>
</div>
<div className="grow" />
</>
)}
<div className="flex gap-2">
<AssetInfoBar asset={asset} />
<UserBar isHelpChatOpen={isHelpChatOpen} setIsHelpChatOpen={setIsHelpChatOpen} />
<UserBar
isHelpChatOpen={isHelpChatOpen}
setIsHelpChatOpen={setIsHelpChatOpen}
onSignOut={onSignOut}
/>
</div>
</div>
)

View File

@ -17,14 +17,15 @@ import UserMenu from './userMenu'
export interface UserBarProps {
isHelpChatOpen: boolean
setIsHelpChatOpen: (isHelpChatOpen: boolean) => void
onSignOut: () => void
}
/** A toolbar containing chat and the user menu. */
export default function UserBar(props: UserBarProps) {
const { isHelpChatOpen, setIsHelpChatOpen } = props
const { isHelpChatOpen, setIsHelpChatOpen, onSignOut } = props
const { updateModal } = modalProvider.useSetModal()
return (
<div className="flex shrink-0 items-center bg-frame-bg rounded-full gap-3 h-8 pl-2 pr-0.75">
<div className="flex shrink-0 items-center bg-frame-bg rounded-full gap-3 h-8 pl-2 pr-0.75 cursor-default pointer-events-auto">
<Button
active={isHelpChatOpen}
image={ChatIcon}
@ -35,7 +36,9 @@ export default function UserBar(props: UserBarProps) {
<button
onClick={event => {
event.stopPropagation()
updateModal(oldModal => (oldModal?.type === UserMenu ? null : <UserMenu />))
updateModal(oldModal =>
oldModal?.type === UserMenu ? null : <UserMenu onSignOut={onSignOut} />
)
}}
>
<img src={DefaultUserIcon} height={28} width={28} />

View File

@ -37,8 +37,14 @@ function UserMenuItem(props: React.PropsWithChildren<UserMenuItemProps>) {
)
}
/** Props for a {@link UserMenu}. */
export interface UserMenuProps {
onSignOut: () => void
}
/** Handling the UserMenuItem click event logic and displaying its content. */
export default function UserMenu() {
export default function UserMenu(props: UserMenuProps) {
const { onSignOut } = props
const { signOut } = auth.useAuth()
const { accessToken, organization } = auth.useNonPartialUserSession()
const navigate = hooks.useNavigate()
@ -84,7 +90,17 @@ export default function UserMenu() {
Change your password
</UserMenuItem>
)}
<UserMenuItem onClick={signOut}>Sign out</UserMenuItem>
<UserMenuItem
onClick={() => {
onSignOut()
// Wait until React has switched back to drive view, before signing out.
window.setTimeout(() => {
void signOut()
}, 0)
}}
>
Sign out
</UserMenuItem>
</>
) : (
<>

View File

@ -12,6 +12,12 @@ body {
animation-duration: 0.2s;
}
.enso-dashboard {
position: absolute;
width: 100%;
height: 100%;
}
/* These styles MUST still be copied
* as `.enso-dashboard body` and `.enso-dashboard html` make no sense. */
.enso-dashboard,
@ -150,4 +156,10 @@ body {
.pointer-events-none-recursive * {
pointer-events: none;
}
/* The class is repeated to make this take precedence over most Tailwind classes. */
.cursor-none-recursive.cursor-none-recursive,
.cursor-none-recursive.cursor-none-recursive * {
cursor: none;
}
}

View File

@ -194,6 +194,7 @@ define_themes! { [light:0, dark:1]
top_bar {
padding_left = 19.0, 19.0;
padding_top = 9.0, 9.0;
gap = 16.0, 16.0;
background {
color = Rgba(1.0, 1.0, 1.0, 0.44), Rgba(0.0, 0.0, 0.0, 0.44);